Skip to main content

asciidoc_parser/blocks/
compound_delimited.rs

1use std::slice::Iter;
2
3use crate::{
4    HasSpan, Parser, Span,
5    attributes::Attrlist,
6    blocks::{
7        Block, ContentModel, IsBlock, caption::assign_block_caption, metadata::BlockMetadata,
8        parse_utils::parse_blocks_until,
9    },
10    internal::debug::DebugSliceReference,
11    span::MatchedItem,
12    strings::CowStr,
13    warnings::{MatchAndWarnings, Warning, WarningType},
14};
15
16/// A delimited block that can contain other blocks.
17///
18/// The following delimiters are recognized as compound delimited blocks:
19///
20/// | Delimiter | Content type |
21/// |-----------|--------------|
22/// | `====`    | Example      |
23/// | `--`      | Open         |
24/// | `****`    | Sidebar      |
25///
26/// The quote delimiter (`____`) is also recognized by `is_valid_delimiter` (so
27/// paragraph and list parsing stop at it), but a `____` block is parsed as a
28/// [`QuoteBlock`](crate::blocks::QuoteBlock) rather than a
29/// `CompoundDelimitedBlock`.
30#[derive(Clone, Eq, PartialEq)]
31pub struct CompoundDelimitedBlock<'src> {
32    blocks: Vec<Block<'src>>,
33    context: CowStr<'src>,
34    source: Span<'src>,
35    title_source: Option<Span<'src>>,
36    title: Option<String>,
37    caption: Option<String>,
38    number: Option<usize>,
39    anchor: Option<Span<'src>>,
40    anchor_reftext: Option<Span<'src>>,
41    attrlist: Option<Attrlist<'src>>,
42}
43
44impl<'src> CompoundDelimitedBlock<'src> {
45    pub(crate) fn is_valid_delimiter(line: &Span<'src>) -> bool {
46        let data = line.data();
47
48        if data == "--" {
49            return true;
50        }
51
52        // Every character after the initial four must match the fourth
53        // (delimiter) character. This matches Asciidoctor, whose
54        // `is_delimited_block?` requires the run following the leading char to
55        // be uniform (see https://github.com/asciidoc-rs/asciidoc-parser/issues/145
56        // and https://gitlab.eclipse.org/eclipse/asciidoc-lang/asciidoc-lang/-/issues/56):
57        // `====xyz` is not a delimiter, but `====` plus any number of `=` is.
58
59        if data.len() >= 4 {
60            if data.starts_with("====") {
61                data.split_at(4).1.chars().all(|c| c == '=')
62            } else if data.starts_with("****") {
63                data.split_at(4).1.chars().all(|c| c == '*')
64            } else if data.starts_with("____") {
65                data.split_at(4).1.chars().all(|c| c == '_')
66            } else {
67                false
68            }
69        } else {
70            false
71        }
72    }
73
74    /// Consume this block and return its nested blocks.
75    pub(crate) fn into_nested_blocks(self) -> Vec<Block<'src>> {
76        self.blocks
77    }
78
79    /// Returns the typed context of this compound delimited block.
80    ///
81    /// A compound delimited block is always one of a small, fixed set of
82    /// contexts (example, open, or sidebar). This accessor lets a converter
83    /// dispatch on that set directly, rather than string-matching the value of
84    /// [`resolved_context`](crate::blocks::IsBlock::resolved_context).
85    pub fn context_kind(&self) -> CompoundDelimitedContext {
86        // `parse` only ever assigns one of these three contexts, so matching
87        // the two most specific and treating the remainder as `Sidebar` keeps
88        // the mapping total without an unreachable arm.
89        match self.context.as_ref() {
90            "example" => CompoundDelimitedContext::Example,
91            "open" => CompoundDelimitedContext::Open,
92            _ => CompoundDelimitedContext::Sidebar,
93        }
94    }
95
96    pub(crate) fn parse(
97        metadata: &BlockMetadata<'src>,
98        parser: &mut Parser,
99    ) -> Option<MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>>> {
100        let delimiter = metadata.block_start.take_normalized_line();
101        let maybe_delimiter_text = delimiter.item.data();
102
103        // An open block is delimited by exactly two hyphens (`--`). Three or
104        // more hyphens do not delimit an open block: `---` is a thematic break
105        // (see `Break`), matching Asciidoctor, which renders a lone `---` as an
106        // `<hr>` and treats `---` inside an open block as literal text. This is
107        // enforced by `is_valid_delimiter`, which accepts only exactly `--`.
108        let context = match maybe_delimiter_text
109            .split_at_checked(maybe_delimiter_text.len().min(4))?
110            .0
111        {
112            "====" => "example",
113            "--" => "open",
114            "****" => "sidebar",
115            // Quote-delimited blocks (`____`) are handled by `QuoteBlock`, which
116            // intercepts them before this parser is reached. `is_valid_delimiter`
117            // still recognizes them so that paragraph and list parsing stop at a
118            // `____` line.
119            _ => return None,
120        };
121
122        if !Self::is_valid_delimiter(&delimiter.item) {
123            return None;
124        }
125
126        let mut next = delimiter.after;
127        let (closing_delimiter, after) = loop {
128            if next.is_empty() {
129                break (next, next);
130            }
131
132            let line = next.take_normalized_line();
133            if line.item.data() == delimiter.item.data() {
134                break (line.item, line.after);
135            }
136            next = line.after;
137        };
138
139        let inside_delimiters = delimiter.after.trim_remainder(closing_delimiter);
140
141        let maw_blocks = parse_blocks_until(inside_delimiters, |_, _| false, parser);
142
143        let blocks = maw_blocks.item;
144        let source = metadata
145            .source
146            .trim_remainder(closing_delimiter.discard_all());
147
148        // The caption (and its number) are assigned here, after the nested
149        // blocks have been parsed, so that a nested captioned block is numbered
150        // before its container (matching Asciidoctor's parse order). Among the
151        // compound contexts only `example` is captionable.
152        let caption = assign_block_caption(
153            parser,
154            context,
155            metadata.attrlist.as_ref(),
156            metadata.title.is_some(),
157        );
158        let number = caption.as_ref().and_then(|c| c.number);
159        let caption = caption.map(|c| c.prefix);
160
161        Some(MatchAndWarnings {
162            item: Some(MatchedItem {
163                item: Self {
164                    blocks: blocks.item,
165                    context: context.into(),
166                    source: source.trim_trailing_whitespace(),
167                    title_source: metadata.title_source,
168                    title: metadata.title.clone(),
169                    caption,
170                    number,
171                    anchor: metadata.anchor,
172                    anchor_reftext: metadata.anchor_reftext,
173                    attrlist: metadata.attrlist.clone(),
174                },
175                after,
176            }),
177            warnings: if closing_delimiter.is_empty() {
178                let mut warnings = maw_blocks.warnings;
179                warnings.insert(
180                    0,
181                    Warning {
182                        source: delimiter.item,
183                        warning: WarningType::UnterminatedDelimitedBlock,
184                        origin: None,
185                    },
186                );
187                warnings
188            } else {
189                maw_blocks.warnings
190            },
191        })
192    }
193}
194
195impl<'src> IsBlock<'src> for CompoundDelimitedBlock<'src> {
196    fn content_model(&self) -> ContentModel {
197        ContentModel::Compound
198    }
199
200    fn raw_context(&self) -> CowStr<'src> {
201        self.context.clone()
202    }
203
204    fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
205        self.blocks.iter()
206    }
207
208    fn nested_blocks_mut(&mut self) -> &mut [Block<'src>] {
209        &mut self.blocks
210    }
211
212    fn title_source(&'src self) -> Option<Span<'src>> {
213        self.title_source
214    }
215
216    fn title(&self) -> Option<&str> {
217        self.title.as_deref()
218    }
219
220    fn caption(&self) -> Option<&str> {
221        self.caption.as_deref()
222    }
223
224    fn number(&self) -> Option<usize> {
225        self.number
226    }
227
228    fn anchor(&'src self) -> Option<Span<'src>> {
229        self.anchor
230    }
231
232    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
233        self.anchor_reftext
234    }
235
236    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
237        self.attrlist.as_ref()
238    }
239}
240
241impl<'src> HasSpan<'src> for CompoundDelimitedBlock<'src> {
242    fn span(&self) -> Span<'src> {
243        self.source
244    }
245}
246
247impl std::fmt::Debug for CompoundDelimitedBlock<'_> {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        f.debug_struct("CompoundDelimitedBlock")
250            .field("blocks", &DebugSliceReference(&self.blocks))
251            .field("context", &self.context)
252            .field("source", &self.source)
253            .field("title_source", &self.title_source)
254            .field("title", &self.title)
255            .field("caption", &self.caption)
256            .field("number", &self.number)
257            .field("anchor", &self.anchor)
258            .field("anchor_reftext", &self.anchor_reftext)
259            .field("attrlist", &self.attrlist)
260            .finish()
261    }
262}
263
264/// The context of a [`CompoundDelimitedBlock`]: the closed set of compound
265/// delimited block types the parser recognizes.
266///
267/// Unlike the stringly-typed
268/// [`resolved_context`](crate::blocks::IsBlock::resolved_context),
269/// this enumerates exactly the contexts a `CompoundDelimitedBlock` can have,
270/// making dispatch over them exhaustive and self-documenting. Use
271/// [`CompoundDelimitedBlock::context_kind`] to obtain it.
272#[derive(Clone, Copy, Eq, PartialEq, Hash)]
273pub enum CompoundDelimitedContext {
274    /// An example block (`====`).
275    Example,
276
277    /// An open block (`--`).
278    Open,
279
280    /// A sidebar block (`****`).
281    Sidebar,
282}
283
284impl CompoundDelimitedContext {
285    /// Returns the canonical context string for this variant (e.g.,
286    /// `"example"`), matching [`resolved_context`].
287    ///
288    /// [`resolved_context`]: crate::blocks::IsBlock::resolved_context
289    pub fn as_str(self) -> &'static str {
290        match self {
291            Self::Example => "example",
292            Self::Open => "open",
293            Self::Sidebar => "sidebar",
294        }
295    }
296}
297
298impl std::fmt::Debug for CompoundDelimitedContext {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        match self {
301            Self::Example => write!(f, "CompoundDelimitedContext::Example"),
302            Self::Open => write!(f, "CompoundDelimitedContext::Open"),
303            Self::Sidebar => write!(f, "CompoundDelimitedContext::Sidebar"),
304        }
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    #![allow(clippy::unwrap_used)]
311
312    use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
313
314    mod context_kind {
315        use std::ops::Deref;
316
317        use crate::blocks::{
318            CompoundDelimitedBlock, CompoundDelimitedContext, IsBlock, metadata::BlockMetadata,
319        };
320
321        fn kind_of(source: &str) -> CompoundDelimitedContext {
322            let mut parser = crate::Parser::default();
323            let maw =
324                CompoundDelimitedBlock::parse(&BlockMetadata::new(source), &mut parser).unwrap();
325            let block = maw.item.unwrap().item;
326
327            // The typed kind must agree with the stringly-typed context.
328            assert_eq!(
329                block.context_kind().as_str(),
330                block.resolved_context().deref()
331            );
332
333            block.context_kind()
334        }
335
336        #[test]
337        fn example() {
338            assert_eq!(
339                kind_of("====\nblah\n===="),
340                CompoundDelimitedContext::Example
341            );
342        }
343
344        #[test]
345        fn open() {
346            assert_eq!(kind_of("--\nblah\n--"), CompoundDelimitedContext::Open);
347        }
348
349        #[test]
350        fn sidebar() {
351            assert_eq!(
352                kind_of("****\nblah\n****"),
353                CompoundDelimitedContext::Sidebar
354            );
355        }
356
357        #[test]
358        fn impl_debug() {
359            assert_eq!(
360                format!("{:?}", CompoundDelimitedContext::Example),
361                "CompoundDelimitedContext::Example"
362            );
363            assert_eq!(
364                format!("{:?}", CompoundDelimitedContext::Open),
365                "CompoundDelimitedContext::Open"
366            );
367            assert_eq!(
368                format!("{:?}", CompoundDelimitedContext::Sidebar),
369                "CompoundDelimitedContext::Sidebar"
370            );
371        }
372    }
373
374    mod is_valid_delimiter {
375        use crate::blocks::CompoundDelimitedBlock;
376
377        #[test]
378        fn comment() {
379            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
380                &crate::Span::new("////")
381            ));
382            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
383                &crate::Span::new("/////")
384            ));
385            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
386                &crate::Span::new("/////////")
387            ));
388
389            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
390                &crate::Span::new("///")
391            ));
392            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
393                &crate::Span::new("//-/")
394            ));
395            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
396                &crate::Span::new("////-")
397            ));
398            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
399                &crate::Span::new("//////////x")
400            ));
401            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
402                &crate::Span::new("//😀/")
403            ));
404        }
405
406        #[test]
407        fn example() {
408            assert!(CompoundDelimitedBlock::is_valid_delimiter(
409                &crate::Span::new("====")
410            ));
411            assert!(CompoundDelimitedBlock::is_valid_delimiter(
412                &crate::Span::new("=====")
413            ));
414            assert!(CompoundDelimitedBlock::is_valid_delimiter(
415                &crate::Span::new("=======")
416            ));
417
418            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
419                &crate::Span::new("===")
420            ));
421            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
422                &crate::Span::new("==-=")
423            ));
424            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
425                &crate::Span::new("====-")
426            ));
427            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
428                &crate::Span::new("==========x")
429            ));
430        }
431
432        #[test]
433        fn listing() {
434            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
435                &crate::Span::new("----")
436            ));
437            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
438                &crate::Span::new("-----")
439            ));
440            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
441                &crate::Span::new("---------")
442            ));
443        }
444
445        #[test]
446        fn literal() {
447            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
448                &crate::Span::new("....")
449            ));
450            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
451                &crate::Span::new(".....")
452            ));
453            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
454                &crate::Span::new(".........")
455            ));
456        }
457
458        #[test]
459        fn sidebar() {
460            assert!(CompoundDelimitedBlock::is_valid_delimiter(
461                &crate::Span::new("****")
462            ));
463            assert!(CompoundDelimitedBlock::is_valid_delimiter(
464                &crate::Span::new("*****")
465            ));
466            assert!(CompoundDelimitedBlock::is_valid_delimiter(
467                &crate::Span::new("*********")
468            ));
469
470            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
471                &crate::Span::new("***")
472            ));
473            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
474                &crate::Span::new("**-*")
475            ));
476            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
477                &crate::Span::new("****-")
478            ));
479            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
480                &crate::Span::new("**********x")
481            ));
482        }
483
484        #[test]
485        fn table() {
486            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
487                &crate::Span::new("|===")
488            ));
489            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
490                &crate::Span::new(",===")
491            ));
492            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
493                &crate::Span::new(":===")
494            ));
495            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
496                &crate::Span::new("!===")
497            ));
498        }
499
500        #[test]
501        fn pass() {
502            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
503                &crate::Span::new("++++")
504            ));
505            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
506                &crate::Span::new("+++++")
507            ));
508            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
509                &crate::Span::new("+++++++++")
510            ));
511        }
512
513        #[test]
514        fn quote() {
515            assert!(CompoundDelimitedBlock::is_valid_delimiter(
516                &crate::Span::new("____")
517            ));
518            assert!(CompoundDelimitedBlock::is_valid_delimiter(
519                &crate::Span::new("_____")
520            ));
521            assert!(CompoundDelimitedBlock::is_valid_delimiter(
522                &crate::Span::new("_________")
523            ));
524
525            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
526                &crate::Span::new("___")
527            ));
528            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
529                &crate::Span::new("__-_")
530            ));
531            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
532                &crate::Span::new("____-")
533            ));
534            assert!(!CompoundDelimitedBlock::is_valid_delimiter(
535                &crate::Span::new("_________x")
536            ));
537        }
538    }
539
540    mod parse {
541        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
542
543        #[test]
544        fn err_invalid_delimiter() {
545            let mut parser = Parser::default();
546            assert!(
547                crate::blocks::CompoundDelimitedBlock::parse(&BlockMetadata::new(""), &mut parser)
548                    .is_none()
549            );
550
551            let mut parser = Parser::default();
552            assert!(
553                crate::blocks::CompoundDelimitedBlock::parse(
554                    &BlockMetadata::new("///"),
555                    &mut parser
556                )
557                .is_none()
558            );
559
560            let mut parser = Parser::default();
561            assert!(
562                crate::blocks::CompoundDelimitedBlock::parse(
563                    &BlockMetadata::new("////x"),
564                    &mut parser
565                )
566                .is_none()
567            );
568
569            let mut parser = Parser::default();
570            assert!(
571                crate::blocks::CompoundDelimitedBlock::parse(
572                    &BlockMetadata::new("--x"),
573                    &mut parser
574                )
575                .is_none()
576            );
577
578            let mut parser = Parser::default();
579            assert!(
580                crate::blocks::CompoundDelimitedBlock::parse(
581                    &BlockMetadata::new("****x"),
582                    &mut parser
583                )
584                .is_none()
585            );
586
587            let mut parser = Parser::default();
588            assert!(
589                crate::blocks::CompoundDelimitedBlock::parse(
590                    &BlockMetadata::new("__\n__"),
591                    &mut parser
592                )
593                .is_none()
594            );
595        }
596
597        #[test]
598        fn err_unterminated() {
599            let mut parser = Parser::default();
600
601            let maw = crate::blocks::CompoundDelimitedBlock::parse(
602                &BlockMetadata::new("====\nblah blah blah"),
603                &mut parser,
604            )
605            .unwrap();
606
607            assert_eq!(
608                maw.item.unwrap().item,
609                CompoundDelimitedBlock {
610                    blocks: &[Block::Simple(SimpleBlock {
611                        content: Content {
612                            original: Span {
613                                data: "blah blah blah",
614                                line: 2,
615                                col: 1,
616                                offset: 5,
617                            },
618                            rendered: "blah blah blah",
619                        },
620                        source: Span {
621                            data: "blah blah blah",
622                            line: 2,
623                            col: 1,
624                            offset: 5,
625                        },
626                        style: SimpleBlockStyle::Paragraph,
627                        title_source: None,
628                        title: None,
629                        caption: None,
630                        number: None,
631                        anchor: None,
632                        anchor_reftext: None,
633                        attrlist: None,
634                    },),],
635                    context: "example",
636                    source: Span {
637                        data: "====\nblah blah blah",
638                        line: 1,
639                        col: 1,
640                        offset: 0,
641                    },
642                    title_source: None,
643                    title: None,
644                    caption: None,
645                    number: None,
646                    anchor: None,
647                    anchor_reftext: None,
648                    attrlist: None,
649                },
650            );
651
652            assert_eq!(
653                maw.warnings,
654                vec![Warning {
655                    source: Span {
656                        data: "====",
657                        line: 1,
658                        col: 1,
659                        offset: 0,
660                    },
661                    warning: WarningType::UnterminatedDelimitedBlock,
662                }]
663            );
664        }
665    }
666
667    mod comment {
668        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
669
670        #[test]
671        fn empty() {
672            let mut parser = Parser::default();
673            assert!(
674                crate::blocks::CompoundDelimitedBlock::parse(
675                    &BlockMetadata::new("////\n////"),
676                    &mut parser
677                )
678                .is_none()
679            );
680        }
681
682        #[test]
683        fn multiple_lines() {
684            let mut parser = Parser::default();
685            assert!(
686                crate::blocks::CompoundDelimitedBlock::parse(
687                    &BlockMetadata::new("////\nline1  \nline2\n////"),
688                    &mut parser
689                )
690                .is_none()
691            );
692        }
693    }
694
695    mod example {
696        use crate::{
697            blocks::{ContentModel, metadata::BlockMetadata},
698            tests::prelude::*,
699        };
700
701        #[test]
702        fn empty() {
703            let mut parser = Parser::default();
704
705            let maw = crate::blocks::CompoundDelimitedBlock::parse(
706                &BlockMetadata::new("====\n===="),
707                &mut parser,
708            )
709            .unwrap();
710
711            let mi = maw.item.unwrap().clone();
712
713            assert_eq!(
714                mi.item,
715                CompoundDelimitedBlock {
716                    blocks: &[],
717                    context: "example",
718                    source: Span {
719                        data: "====\n====",
720                        line: 1,
721                        col: 1,
722                        offset: 0,
723                    },
724                    title_source: None,
725                    title: None,
726                    caption: None,
727                    number: None,
728                    anchor: None,
729                    anchor_reftext: None,
730                    attrlist: None,
731                }
732            );
733
734            assert_eq!(mi.item.content_model(), ContentModel::Compound);
735            assert!(mi.item.rendered_content().is_none());
736            assert_eq!(mi.item.raw_context().as_ref(), "example");
737            assert_eq!(mi.item.resolved_context().as_ref(), "example");
738            assert!(mi.item.declared_style().is_none());
739            assert!(mi.item.nested_blocks().next().is_none());
740            assert!(mi.item.id().is_none());
741            assert!(mi.item.roles().is_empty());
742            assert!(mi.item.options().is_empty());
743            assert!(mi.item.title_source().is_none());
744            assert!(mi.item.title().is_none());
745            assert!(mi.item.anchor().is_none());
746            assert!(mi.item.anchor_reftext().is_none());
747            assert!(mi.item.attrlist().is_none());
748            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
749        }
750
751        #[test]
752        fn multiple_blocks() {
753            let mut parser = Parser::default();
754
755            let maw = crate::blocks::CompoundDelimitedBlock::parse(
756                &BlockMetadata::new("====\nblock1\n\nblock2\n===="),
757                &mut parser,
758            )
759            .unwrap();
760
761            let mi = maw.item.unwrap().clone();
762
763            assert_eq!(
764                mi.item,
765                CompoundDelimitedBlock {
766                    blocks: &[
767                        Block::Simple(SimpleBlock {
768                            content: Content {
769                                original: Span {
770                                    data: "block1",
771                                    line: 2,
772                                    col: 1,
773                                    offset: 5,
774                                },
775                                rendered: "block1",
776                            },
777                            source: Span {
778                                data: "block1",
779                                line: 2,
780                                col: 1,
781                                offset: 5,
782                            },
783                            style: SimpleBlockStyle::Paragraph,
784                            title_source: None,
785                            title: None,
786                            caption: None,
787                            number: None,
788                            anchor: None,
789                            anchor_reftext: None,
790                            attrlist: None,
791                        },),
792                        Block::Simple(SimpleBlock {
793                            content: Content {
794                                original: Span {
795                                    data: "block2",
796                                    line: 4,
797                                    col: 1,
798                                    offset: 13,
799                                },
800                                rendered: "block2",
801                            },
802                            source: Span {
803                                data: "block2",
804                                line: 4,
805                                col: 1,
806                                offset: 13,
807                            },
808                            style: SimpleBlockStyle::Paragraph,
809                            title_source: None,
810                            title: None,
811                            caption: None,
812                            number: None,
813                            anchor: None,
814                            anchor_reftext: None,
815                            attrlist: None,
816                        },),
817                    ],
818                    context: "example",
819                    source: Span {
820                        data: "====\nblock1\n\nblock2\n====",
821                        line: 1,
822                        col: 1,
823                        offset: 0,
824                    },
825                    title_source: None,
826                    title: None,
827                    caption: None,
828                    number: None,
829                    anchor: None,
830                    anchor_reftext: None,
831                    attrlist: None,
832                }
833            );
834
835            assert_eq!(mi.item.content_model(), ContentModel::Compound);
836            assert_eq!(mi.item.raw_context().as_ref(), "example");
837            assert_eq!(mi.item.resolved_context().as_ref(), "example");
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::Normal);
848
849            let mut blocks = mi.item.nested_blocks();
850            assert_eq!(
851                blocks.next().unwrap(),
852                &Block::Simple(SimpleBlock {
853                    content: Content {
854                        original: Span {
855                            data: "block1",
856                            line: 2,
857                            col: 1,
858                            offset: 5,
859                        },
860                        rendered: "block1",
861                    },
862                    source: Span {
863                        data: "block1",
864                        line: 2,
865                        col: 1,
866                        offset: 5,
867                    },
868                    style: SimpleBlockStyle::Paragraph,
869                    title_source: None,
870                    title: None,
871                    caption: None,
872                    number: None,
873                    anchor: None,
874                    anchor_reftext: None,
875                    attrlist: None,
876                },)
877            );
878
879            assert_eq!(
880                blocks.next().unwrap(),
881                &Block::Simple(SimpleBlock {
882                    content: Content {
883                        original: Span {
884                            data: "block2",
885                            line: 4,
886                            col: 1,
887                            offset: 13,
888                        },
889                        rendered: "block2",
890                    },
891                    source: Span {
892                        data: "block2",
893                        line: 4,
894                        col: 1,
895                        offset: 13,
896                    },
897                    style: SimpleBlockStyle::Paragraph,
898                    title_source: None,
899                    title: None,
900                    caption: None,
901                    number: None,
902                    anchor: None,
903                    anchor_reftext: None,
904                    attrlist: None,
905                },)
906            );
907
908            assert!(blocks.next().is_none());
909        }
910
911        #[test]
912        fn nested_blocks() {
913            let mut parser = Parser::default();
914
915            let maw = crate::blocks::CompoundDelimitedBlock::parse(
916                &BlockMetadata::new("====\nblock1\n\n=====\nblock2\n=====\n===="),
917                &mut parser,
918            )
919            .unwrap();
920
921            let mi = maw.item.unwrap().clone();
922
923            assert_eq!(
924                mi.item,
925                CompoundDelimitedBlock {
926                    blocks: &[
927                        Block::Simple(SimpleBlock {
928                            content: Content {
929                                original: Span {
930                                    data: "block1",
931                                    line: 2,
932                                    col: 1,
933                                    offset: 5,
934                                },
935                                rendered: "block1",
936                            },
937                            source: Span {
938                                data: "block1",
939                                line: 2,
940                                col: 1,
941                                offset: 5,
942                            },
943                            style: SimpleBlockStyle::Paragraph,
944                            title_source: None,
945                            title: None,
946                            caption: None,
947                            number: None,
948                            anchor: None,
949                            anchor_reftext: None,
950                            attrlist: None,
951                        },),
952                        Block::CompoundDelimited(CompoundDelimitedBlock {
953                            blocks: &[Block::Simple(SimpleBlock {
954                                content: Content {
955                                    original: Span {
956                                        data: "block2",
957                                        line: 5,
958                                        col: 1,
959                                        offset: 19,
960                                    },
961                                    rendered: "block2",
962                                },
963                                source: Span {
964                                    data: "block2",
965                                    line: 5,
966                                    col: 1,
967                                    offset: 19,
968                                },
969                                style: SimpleBlockStyle::Paragraph,
970                                title_source: None,
971                                title: None,
972                                caption: None,
973                                number: None,
974                                anchor: None,
975                                anchor_reftext: None,
976                                attrlist: None,
977                            },),],
978                            context: "example",
979                            source: Span {
980                                data: "=====\nblock2\n=====",
981                                line: 4,
982                                col: 1,
983                                offset: 13,
984                            },
985                            title_source: None,
986                            title: None,
987                            caption: None,
988                            number: None,
989                            anchor: None,
990                            anchor_reftext: None,
991                            attrlist: None,
992                        })
993                    ],
994                    context: "example",
995                    source: Span {
996                        data: "====\nblock1\n\n=====\nblock2\n=====\n====",
997                        line: 1,
998                        col: 1,
999                        offset: 0,
1000                    },
1001                    title_source: None,
1002                    title: None,
1003                    caption: None,
1004                    number: None,
1005                    anchor: None,
1006                    anchor_reftext: None,
1007                    attrlist: None,
1008                }
1009            );
1010
1011            assert_eq!(mi.item.content_model(), ContentModel::Compound);
1012            assert_eq!(mi.item.raw_context().as_ref(), "example");
1013            assert_eq!(mi.item.resolved_context().as_ref(), "example");
1014            assert!(mi.item.declared_style().is_none());
1015            assert!(mi.item.id().is_none());
1016            assert!(mi.item.roles().is_empty());
1017            assert!(mi.item.options().is_empty());
1018            assert!(mi.item.title_source().is_none());
1019            assert!(mi.item.title().is_none());
1020            assert!(mi.item.anchor().is_none());
1021            assert!(mi.item.anchor_reftext().is_none());
1022            assert!(mi.item.attrlist().is_none());
1023            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1024
1025            let mut blocks = mi.item.nested_blocks();
1026            assert_eq!(
1027                blocks.next().unwrap(),
1028                &Block::Simple(SimpleBlock {
1029                    content: Content {
1030                        original: Span {
1031                            data: "block1",
1032                            line: 2,
1033                            col: 1,
1034                            offset: 5,
1035                        },
1036                        rendered: "block1",
1037                    },
1038                    source: Span {
1039                        data: "block1",
1040                        line: 2,
1041                        col: 1,
1042                        offset: 5,
1043                    },
1044                    style: SimpleBlockStyle::Paragraph,
1045                    title_source: None,
1046                    title: None,
1047                    caption: None,
1048                    number: None,
1049                    anchor: None,
1050                    anchor_reftext: None,
1051                    attrlist: None,
1052                },)
1053            );
1054
1055            assert_eq!(
1056                blocks.next().unwrap(),
1057                &Block::CompoundDelimited(CompoundDelimitedBlock {
1058                    blocks: &[Block::Simple(SimpleBlock {
1059                        content: Content {
1060                            original: Span {
1061                                data: "block2",
1062                                line: 5,
1063                                col: 1,
1064                                offset: 19,
1065                            },
1066                            rendered: "block2",
1067                        },
1068                        source: Span {
1069                            data: "block2",
1070                            line: 5,
1071                            col: 1,
1072                            offset: 19,
1073                        },
1074                        style: SimpleBlockStyle::Paragraph,
1075                        title_source: None,
1076                        title: None,
1077                        caption: None,
1078                        number: None,
1079                        anchor: None,
1080                        anchor_reftext: None,
1081                        attrlist: None,
1082                    },),],
1083                    context: "example",
1084                    source: Span {
1085                        data: "=====\nblock2\n=====",
1086                        line: 4,
1087                        col: 1,
1088                        offset: 13,
1089                    },
1090                    title_source: None,
1091                    title: None,
1092                    caption: None,
1093                    number: None,
1094                    anchor: None,
1095                    anchor_reftext: None,
1096                    attrlist: None,
1097                })
1098            );
1099
1100            assert!(blocks.next().is_none());
1101        }
1102        #[test]
1103        fn no_panic_for_utf8_code_point_using_more_than_one_byte() {
1104            let mut parser = Parser::default();
1105            assert!(
1106                crate::blocks::CompoundDelimitedBlock::parse(
1107                    &BlockMetadata::new("===😀"),
1108                    &mut parser
1109                )
1110                .is_none()
1111            );
1112        }
1113    }
1114
1115    mod listing {
1116        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1117
1118        #[test]
1119        fn empty() {
1120            let mut parser = Parser::default();
1121            assert!(
1122                crate::blocks::CompoundDelimitedBlock::parse(
1123                    &BlockMetadata::new("----\n----"),
1124                    &mut parser
1125                )
1126                .is_none()
1127            );
1128        }
1129
1130        #[test]
1131        fn multiple_lines() {
1132            let mut parser = Parser::default();
1133            assert!(
1134                crate::blocks::CompoundDelimitedBlock::parse(
1135                    &BlockMetadata::new("----\nline1  \nline2\n----"),
1136                    &mut parser
1137                )
1138                .is_none()
1139            );
1140        }
1141    }
1142
1143    mod literal {
1144        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1145
1146        #[test]
1147        fn empty() {
1148            let mut parser = Parser::default();
1149            assert!(
1150                crate::blocks::CompoundDelimitedBlock::parse(
1151                    &BlockMetadata::new("....\n...."),
1152                    &mut parser
1153                )
1154                .is_none()
1155            );
1156        }
1157
1158        #[test]
1159        fn multiple_lines() {
1160            let mut parser = Parser::default();
1161            assert!(
1162                crate::blocks::CompoundDelimitedBlock::parse(
1163                    &BlockMetadata::new("....\nline1  \nline2\n...."),
1164                    &mut parser
1165                )
1166                .is_none()
1167            );
1168        }
1169    }
1170
1171    mod open {
1172        use crate::{
1173            blocks::{BreakType, ContentModel, metadata::BlockMetadata},
1174            tests::prelude::*,
1175        };
1176
1177        #[test]
1178        fn empty() {
1179            let mut parser = Parser::default();
1180
1181            let maw = crate::blocks::CompoundDelimitedBlock::parse(
1182                &BlockMetadata::new("--\n--"),
1183                &mut parser,
1184            )
1185            .unwrap();
1186
1187            let mi = maw.item.unwrap().clone();
1188
1189            assert_eq!(
1190                mi.item,
1191                CompoundDelimitedBlock {
1192                    blocks: &[],
1193                    context: "open",
1194                    source: Span {
1195                        data: "--\n--",
1196                        line: 1,
1197                        col: 1,
1198                        offset: 0,
1199                    },
1200                    title_source: None,
1201                    title: None,
1202                    caption: None,
1203                    number: None,
1204                    anchor: None,
1205                    anchor_reftext: None,
1206                    attrlist: None,
1207                }
1208            );
1209
1210            assert_eq!(mi.item.content_model(), ContentModel::Compound);
1211            assert_eq!(mi.item.raw_context().as_ref(), "open");
1212            assert_eq!(mi.item.resolved_context().as_ref(), "open");
1213            assert!(mi.item.declared_style().is_none());
1214            assert!(mi.item.nested_blocks().next().is_none());
1215            assert!(mi.item.id().is_none());
1216            assert!(mi.item.roles().is_empty());
1217            assert!(mi.item.options().is_empty());
1218            assert!(mi.item.title_source().is_none());
1219            assert!(mi.item.title().is_none());
1220            assert!(mi.item.anchor().is_none());
1221            assert!(mi.item.anchor_reftext().is_none());
1222            assert!(mi.item.attrlist().is_none());
1223            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1224        }
1225
1226        #[test]
1227        fn multiple_blocks() {
1228            let mut parser = Parser::default();
1229
1230            let maw = crate::blocks::CompoundDelimitedBlock::parse(
1231                &BlockMetadata::new("--\nblock1\n\nblock2\n--"),
1232                &mut parser,
1233            )
1234            .unwrap();
1235
1236            let mi = maw.item.unwrap().clone();
1237
1238            assert_eq!(
1239                mi.item,
1240                CompoundDelimitedBlock {
1241                    blocks: &[
1242                        Block::Simple(SimpleBlock {
1243                            content: Content {
1244                                original: Span {
1245                                    data: "block1",
1246                                    line: 2,
1247                                    col: 1,
1248                                    offset: 3,
1249                                },
1250                                rendered: "block1",
1251                            },
1252                            source: Span {
1253                                data: "block1",
1254                                line: 2,
1255                                col: 1,
1256                                offset: 3,
1257                            },
1258                            style: SimpleBlockStyle::Paragraph,
1259                            title_source: None,
1260                            title: None,
1261                            caption: None,
1262                            number: None,
1263                            anchor: None,
1264                            anchor_reftext: None,
1265                            attrlist: None,
1266                        },),
1267                        Block::Simple(SimpleBlock {
1268                            content: Content {
1269                                original: Span {
1270                                    data: "block2",
1271                                    line: 4,
1272                                    col: 1,
1273                                    offset: 11,
1274                                },
1275                                rendered: "block2",
1276                            },
1277                            source: Span {
1278                                data: "block2",
1279                                line: 4,
1280                                col: 1,
1281                                offset: 11,
1282                            },
1283                            style: SimpleBlockStyle::Paragraph,
1284                            title_source: None,
1285                            title: None,
1286                            caption: None,
1287                            number: None,
1288                            anchor: None,
1289                            anchor_reftext: None,
1290                            attrlist: None,
1291                        },),
1292                    ],
1293                    context: "open",
1294                    source: Span {
1295                        data: "--\nblock1\n\nblock2\n--",
1296                        line: 1,
1297                        col: 1,
1298                        offset: 0,
1299                    },
1300                    title_source: None,
1301                    title: None,
1302                    caption: None,
1303                    number: None,
1304                    anchor: None,
1305                    anchor_reftext: None,
1306                    attrlist: None,
1307                }
1308            );
1309
1310            assert_eq!(mi.item.content_model(), ContentModel::Compound);
1311            assert_eq!(mi.item.raw_context().as_ref(), "open");
1312            assert_eq!(mi.item.resolved_context().as_ref(), "open");
1313            assert!(mi.item.declared_style().is_none());
1314            assert!(mi.item.id().is_none());
1315            assert!(mi.item.roles().is_empty());
1316            assert!(mi.item.options().is_empty());
1317            assert!(mi.item.title_source().is_none());
1318            assert!(mi.item.title().is_none());
1319            assert!(mi.item.anchor().is_none());
1320            assert!(mi.item.anchor_reftext().is_none());
1321            assert!(mi.item.attrlist().is_none());
1322            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1323
1324            let mut blocks = mi.item.nested_blocks();
1325            assert_eq!(
1326                blocks.next().unwrap(),
1327                &Block::Simple(SimpleBlock {
1328                    content: Content {
1329                        original: Span {
1330                            data: "block1",
1331                            line: 2,
1332                            col: 1,
1333                            offset: 3,
1334                        },
1335                        rendered: "block1",
1336                    },
1337                    source: Span {
1338                        data: "block1",
1339                        line: 2,
1340                        col: 1,
1341                        offset: 3,
1342                    },
1343                    style: SimpleBlockStyle::Paragraph,
1344                    title_source: None,
1345                    title: None,
1346                    caption: None,
1347                    number: None,
1348                    anchor: None,
1349                    anchor_reftext: None,
1350                    attrlist: None,
1351                },)
1352            );
1353
1354            assert_eq!(
1355                blocks.next().unwrap(),
1356                &Block::Simple(SimpleBlock {
1357                    content: Content {
1358                        original: Span {
1359                            data: "block2",
1360                            line: 4,
1361                            col: 1,
1362                            offset: 11,
1363                        },
1364                        rendered: "block2",
1365                    },
1366                    source: Span {
1367                        data: "block2",
1368                        line: 4,
1369                        col: 1,
1370                        offset: 11,
1371                    },
1372                    style: SimpleBlockStyle::Paragraph,
1373                    title_source: None,
1374                    title: None,
1375                    caption: None,
1376                    number: None,
1377                    anchor: None,
1378                    anchor_reftext: None,
1379                    attrlist: None,
1380                },)
1381            );
1382
1383            assert!(blocks.next().is_none());
1384        }
1385
1386        #[test]
1387        fn nested_blocks() {
1388            // Spec says three hyphens does NOT mark an open block.
1389            let mut parser = Parser::default();
1390
1391            let maw = crate::blocks::CompoundDelimitedBlock::parse(
1392                &BlockMetadata::new("--\nblock1\n\n---\nblock2\n---\n--"),
1393                &mut parser,
1394            )
1395            .unwrap();
1396
1397            let mi = maw.item.unwrap().clone();
1398
1399            assert_eq!(
1400                mi.item,
1401                CompoundDelimitedBlock {
1402                    blocks: &[
1403                        Block::Simple(SimpleBlock {
1404                            content: Content {
1405                                original: Span {
1406                                    data: "block1",
1407                                    line: 2,
1408                                    col: 1,
1409                                    offset: 3,
1410                                },
1411                                rendered: "block1",
1412                            },
1413                            source: Span {
1414                                data: "block1",
1415                                line: 2,
1416                                col: 1,
1417                                offset: 3,
1418                            },
1419                            style: SimpleBlockStyle::Paragraph,
1420                            title_source: None,
1421                            title: None,
1422                            caption: None,
1423                            number: None,
1424                            anchor: None,
1425                            anchor_reftext: None,
1426                            attrlist: None,
1427                        },),
1428                        Block::Break(Break {
1429                            type_: BreakType::Thematic,
1430                            source: Span {
1431                                data: "---",
1432                                line: 4,
1433                                col: 1,
1434                                offset: 11,
1435                            },
1436                            title_source: None,
1437                            title: None,
1438                            anchor: None,
1439                            attrlist: None,
1440                        },),
1441                        Block::Simple(SimpleBlock {
1442                            content: Content {
1443                                original: Span {
1444                                    data: "block2\n---",
1445                                    line: 5,
1446                                    col: 1,
1447                                    offset: 15,
1448                                },
1449                                rendered: "block2\n---",
1450                            },
1451                            source: Span {
1452                                data: "block2\n---",
1453                                line: 5,
1454                                col: 1,
1455                                offset: 15,
1456                            },
1457                            style: SimpleBlockStyle::Paragraph,
1458                            title_source: None,
1459                            title: None,
1460                            caption: None,
1461                            number: None,
1462                            anchor: None,
1463                            anchor_reftext: None,
1464                            attrlist: None,
1465                        },),
1466                    ],
1467                    context: "open",
1468                    source: Span {
1469                        data: "--\nblock1\n\n---\nblock2\n---\n--",
1470                        line: 1,
1471                        col: 1,
1472                        offset: 0,
1473                    },
1474                    title_source: None,
1475                    title: None,
1476                    caption: None,
1477                    number: None,
1478                    anchor: None,
1479                    anchor_reftext: None,
1480                    attrlist: None,
1481                }
1482            );
1483
1484            assert_eq!(mi.item.content_model(), ContentModel::Compound);
1485            assert_eq!(mi.item.raw_context().as_ref(), "open");
1486            assert_eq!(mi.item.resolved_context().as_ref(), "open");
1487            assert!(mi.item.declared_style().is_none());
1488            assert!(mi.item.id().is_none());
1489            assert!(mi.item.roles().is_empty());
1490            assert!(mi.item.options().is_empty());
1491            assert!(mi.item.title_source().is_none());
1492            assert!(mi.item.title().is_none());
1493            assert!(mi.item.anchor().is_none());
1494            assert!(mi.item.anchor_reftext().is_none());
1495            assert!(mi.item.attrlist().is_none());
1496            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1497
1498            let mut blocks = mi.item.nested_blocks();
1499            assert_eq!(
1500                blocks.next().unwrap(),
1501                &Block::Simple(SimpleBlock {
1502                    content: Content {
1503                        original: Span {
1504                            data: "block1",
1505                            line: 2,
1506                            col: 1,
1507                            offset: 3,
1508                        },
1509                        rendered: "block1",
1510                    },
1511                    source: Span {
1512                        data: "block1",
1513                        line: 2,
1514                        col: 1,
1515                        offset: 3,
1516                    },
1517                    style: SimpleBlockStyle::Paragraph,
1518                    title_source: None,
1519                    title: None,
1520                    caption: None,
1521                    number: None,
1522                    anchor: None,
1523                    anchor_reftext: None,
1524                    attrlist: None,
1525                },)
1526            );
1527
1528            assert_eq!(
1529                blocks.next().unwrap(),
1530                &Block::Break(Break {
1531                    type_: BreakType::Thematic,
1532                    source: Span {
1533                        data: "---",
1534                        line: 4,
1535                        col: 1,
1536                        offset: 11,
1537                    },
1538                    title_source: None,
1539                    title: None,
1540                    anchor: None,
1541                    attrlist: None,
1542                },)
1543            );
1544
1545            assert_eq!(
1546                blocks.next().unwrap(),
1547                &Block::Simple(SimpleBlock {
1548                    content: Content {
1549                        original: Span {
1550                            data: "block2\n---",
1551                            line: 5,
1552                            col: 1,
1553                            offset: 15,
1554                        },
1555                        rendered: "block2\n---",
1556                    },
1557                    source: Span {
1558                        data: "block2\n---",
1559                        line: 5,
1560                        col: 1,
1561                        offset: 15,
1562                    },
1563                    style: SimpleBlockStyle::Paragraph,
1564                    title_source: None,
1565                    title: None,
1566                    caption: None,
1567                    number: None,
1568                    anchor: None,
1569                    anchor_reftext: None,
1570                    attrlist: None,
1571                },)
1572            );
1573
1574            assert!(blocks.next().is_none());
1575        }
1576    }
1577
1578    mod sidebar {
1579        use crate::{
1580            blocks::{ContentModel, metadata::BlockMetadata},
1581            tests::prelude::*,
1582        };
1583
1584        #[test]
1585        fn empty() {
1586            let mut parser = Parser::default();
1587
1588            let maw = crate::blocks::CompoundDelimitedBlock::parse(
1589                &BlockMetadata::new("****\n****"),
1590                &mut parser,
1591            )
1592            .unwrap();
1593
1594            let mi = maw.item.unwrap().clone();
1595
1596            assert_eq!(
1597                mi.item,
1598                CompoundDelimitedBlock {
1599                    blocks: &[],
1600                    context: "sidebar",
1601                    source: Span {
1602                        data: "****\n****",
1603                        line: 1,
1604                        col: 1,
1605                        offset: 0,
1606                    },
1607                    title_source: None,
1608                    title: None,
1609                    caption: None,
1610                    number: None,
1611                    anchor: None,
1612                    anchor_reftext: None,
1613                    attrlist: None,
1614                }
1615            );
1616
1617            assert_eq!(mi.item.content_model(), ContentModel::Compound);
1618            assert_eq!(mi.item.raw_context().as_ref(), "sidebar");
1619            assert_eq!(mi.item.resolved_context().as_ref(), "sidebar");
1620            assert!(mi.item.declared_style().is_none());
1621            assert!(mi.item.nested_blocks().next().is_none());
1622            assert!(mi.item.id().is_none());
1623            assert!(mi.item.roles().is_empty());
1624            assert!(mi.item.options().is_empty());
1625            assert!(mi.item.title_source().is_none());
1626            assert!(mi.item.title().is_none());
1627            assert!(mi.item.anchor().is_none());
1628            assert!(mi.item.anchor_reftext().is_none());
1629            assert!(mi.item.attrlist().is_none());
1630            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1631        }
1632
1633        #[test]
1634        fn multiple_blocks() {
1635            let mut parser = Parser::default();
1636
1637            let maw = crate::blocks::CompoundDelimitedBlock::parse(
1638                &BlockMetadata::new("****\nblock1\n\nblock2\n****"),
1639                &mut parser,
1640            )
1641            .unwrap();
1642
1643            let mi = maw.item.unwrap().clone();
1644
1645            assert_eq!(
1646                mi.item,
1647                CompoundDelimitedBlock {
1648                    blocks: &[
1649                        Block::Simple(SimpleBlock {
1650                            content: Content {
1651                                original: Span {
1652                                    data: "block1",
1653                                    line: 2,
1654                                    col: 1,
1655                                    offset: 5,
1656                                },
1657                                rendered: "block1",
1658                            },
1659                            source: Span {
1660                                data: "block1",
1661                                line: 2,
1662                                col: 1,
1663                                offset: 5,
1664                            },
1665                            style: SimpleBlockStyle::Paragraph,
1666                            title_source: None,
1667                            title: None,
1668                            caption: None,
1669                            number: None,
1670                            anchor: None,
1671                            anchor_reftext: None,
1672                            attrlist: None,
1673                        },),
1674                        Block::Simple(SimpleBlock {
1675                            content: Content {
1676                                original: Span {
1677                                    data: "block2",
1678                                    line: 4,
1679                                    col: 1,
1680                                    offset: 13,
1681                                },
1682                                rendered: "block2",
1683                            },
1684                            source: Span {
1685                                data: "block2",
1686                                line: 4,
1687                                col: 1,
1688                                offset: 13,
1689                            },
1690                            style: SimpleBlockStyle::Paragraph,
1691                            title_source: None,
1692                            title: None,
1693                            caption: None,
1694                            number: None,
1695                            anchor: None,
1696                            anchor_reftext: None,
1697                            attrlist: None,
1698                        },),
1699                    ],
1700                    context: "sidebar",
1701                    source: Span {
1702                        data: "****\nblock1\n\nblock2\n****",
1703                        line: 1,
1704                        col: 1,
1705                        offset: 0,
1706                    },
1707                    title_source: None,
1708                    title: None,
1709                    caption: None,
1710                    number: None,
1711                    anchor: None,
1712                    anchor_reftext: None,
1713                    attrlist: None,
1714                }
1715            );
1716
1717            assert_eq!(mi.item.content_model(), ContentModel::Compound);
1718            assert_eq!(mi.item.raw_context().as_ref(), "sidebar");
1719            assert_eq!(mi.item.resolved_context().as_ref(), "sidebar");
1720            assert!(mi.item.declared_style().is_none());
1721            assert!(mi.item.id().is_none());
1722            assert!(mi.item.roles().is_empty());
1723            assert!(mi.item.options().is_empty());
1724            assert!(mi.item.title_source().is_none());
1725            assert!(mi.item.title().is_none());
1726            assert!(mi.item.anchor().is_none());
1727            assert!(mi.item.anchor_reftext().is_none());
1728            assert!(mi.item.attrlist().is_none());
1729            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1730
1731            let mut blocks = mi.item.nested_blocks();
1732            assert_eq!(
1733                blocks.next().unwrap(),
1734                &Block::Simple(SimpleBlock {
1735                    content: Content {
1736                        original: Span {
1737                            data: "block1",
1738                            line: 2,
1739                            col: 1,
1740                            offset: 5,
1741                        },
1742                        rendered: "block1",
1743                    },
1744                    source: Span {
1745                        data: "block1",
1746                        line: 2,
1747                        col: 1,
1748                        offset: 5,
1749                    },
1750                    style: SimpleBlockStyle::Paragraph,
1751                    title_source: None,
1752                    title: None,
1753                    caption: None,
1754                    number: None,
1755                    anchor: None,
1756                    anchor_reftext: None,
1757                    attrlist: None,
1758                },)
1759            );
1760
1761            assert_eq!(
1762                blocks.next().unwrap(),
1763                &Block::Simple(SimpleBlock {
1764                    content: Content {
1765                        original: Span {
1766                            data: "block2",
1767                            line: 4,
1768                            col: 1,
1769                            offset: 13,
1770                        },
1771                        rendered: "block2",
1772                    },
1773                    source: Span {
1774                        data: "block2",
1775                        line: 4,
1776                        col: 1,
1777                        offset: 13,
1778                    },
1779                    style: SimpleBlockStyle::Paragraph,
1780                    title_source: None,
1781                    title: None,
1782                    caption: None,
1783                    number: None,
1784                    anchor: None,
1785                    anchor_reftext: None,
1786                    attrlist: None,
1787                },)
1788            );
1789
1790            assert!(blocks.next().is_none());
1791        }
1792
1793        #[test]
1794        fn nested_blocks() {
1795            let mut parser = Parser::default();
1796
1797            let maw = crate::blocks::CompoundDelimitedBlock::parse(
1798                &BlockMetadata::new("****\nblock1\n\n*****\nblock2\n*****\n****"),
1799                &mut parser,
1800            )
1801            .unwrap();
1802
1803            let mi = maw.item.unwrap().clone();
1804
1805            assert_eq!(
1806                mi.item,
1807                CompoundDelimitedBlock {
1808                    blocks: &[
1809                        Block::Simple(SimpleBlock {
1810                            content: Content {
1811                                original: Span {
1812                                    data: "block1",
1813                                    line: 2,
1814                                    col: 1,
1815                                    offset: 5,
1816                                },
1817                                rendered: "block1",
1818                            },
1819                            source: Span {
1820                                data: "block1",
1821                                line: 2,
1822                                col: 1,
1823                                offset: 5,
1824                            },
1825                            style: SimpleBlockStyle::Paragraph,
1826                            title_source: None,
1827                            title: None,
1828                            caption: None,
1829                            number: None,
1830                            anchor: None,
1831                            anchor_reftext: None,
1832                            attrlist: None,
1833                        },),
1834                        Block::CompoundDelimited(CompoundDelimitedBlock {
1835                            blocks: &[Block::Simple(SimpleBlock {
1836                                content: Content {
1837                                    original: Span {
1838                                        data: "block2",
1839                                        line: 5,
1840                                        col: 1,
1841                                        offset: 19,
1842                                    },
1843                                    rendered: "block2",
1844                                },
1845                                source: Span {
1846                                    data: "block2",
1847                                    line: 5,
1848                                    col: 1,
1849                                    offset: 19,
1850                                },
1851                                style: SimpleBlockStyle::Paragraph,
1852                                title_source: None,
1853                                title: None,
1854                                caption: None,
1855                                number: None,
1856                                anchor: None,
1857                                anchor_reftext: None,
1858                                attrlist: None,
1859                            },),],
1860                            context: "sidebar",
1861                            source: Span {
1862                                data: "*****\nblock2\n*****",
1863                                line: 4,
1864                                col: 1,
1865                                offset: 13,
1866                            },
1867                            title_source: None,
1868                            title: None,
1869                            caption: None,
1870                            number: None,
1871                            anchor: None,
1872                            anchor_reftext: None,
1873                            attrlist: None,
1874                        })
1875                    ],
1876                    context: "sidebar",
1877                    source: Span {
1878                        data: "****\nblock1\n\n*****\nblock2\n*****\n****",
1879                        line: 1,
1880                        col: 1,
1881                        offset: 0,
1882                    },
1883                    title_source: None,
1884                    title: None,
1885                    caption: None,
1886                    number: None,
1887                    anchor: None,
1888                    anchor_reftext: None,
1889                    attrlist: None,
1890                }
1891            );
1892
1893            assert_eq!(mi.item.content_model(), ContentModel::Compound);
1894            assert_eq!(mi.item.raw_context().as_ref(), "sidebar");
1895            assert_eq!(mi.item.resolved_context().as_ref(), "sidebar");
1896            assert!(mi.item.declared_style().is_none());
1897            assert!(mi.item.id().is_none());
1898            assert!(mi.item.roles().is_empty());
1899            assert!(mi.item.options().is_empty());
1900            assert!(mi.item.title_source().is_none());
1901            assert!(mi.item.title().is_none());
1902            assert!(mi.item.anchor().is_none());
1903            assert!(mi.item.anchor_reftext().is_none());
1904            assert!(mi.item.attrlist().is_none());
1905            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1906
1907            let mut blocks = mi.item.nested_blocks();
1908            assert_eq!(
1909                blocks.next().unwrap(),
1910                &Block::Simple(SimpleBlock {
1911                    content: Content {
1912                        original: Span {
1913                            data: "block1",
1914                            line: 2,
1915                            col: 1,
1916                            offset: 5,
1917                        },
1918                        rendered: "block1",
1919                    },
1920                    source: Span {
1921                        data: "block1",
1922                        line: 2,
1923                        col: 1,
1924                        offset: 5,
1925                    },
1926                    style: SimpleBlockStyle::Paragraph,
1927                    title_source: None,
1928                    title: None,
1929                    caption: None,
1930                    number: None,
1931                    anchor: None,
1932                    anchor_reftext: None,
1933                    attrlist: None,
1934                },)
1935            );
1936
1937            assert_eq!(
1938                blocks.next().unwrap(),
1939                &Block::CompoundDelimited(CompoundDelimitedBlock {
1940                    blocks: &[Block::Simple(SimpleBlock {
1941                        content: Content {
1942                            original: Span {
1943                                data: "block2",
1944                                line: 5,
1945                                col: 1,
1946                                offset: 19,
1947                            },
1948                            rendered: "block2",
1949                        },
1950                        source: Span {
1951                            data: "block2",
1952                            line: 5,
1953                            col: 1,
1954                            offset: 19,
1955                        },
1956                        style: SimpleBlockStyle::Paragraph,
1957                        title_source: None,
1958                        title: None,
1959                        caption: None,
1960                        number: None,
1961                        anchor: None,
1962                        anchor_reftext: None,
1963                        attrlist: None,
1964                    },),],
1965                    context: "sidebar",
1966                    source: Span {
1967                        data: "*****\nblock2\n*****",
1968                        line: 4,
1969                        col: 1,
1970                        offset: 13,
1971                    },
1972                    title_source: None,
1973                    title: None,
1974                    caption: None,
1975                    number: None,
1976                    anchor: None,
1977                    anchor_reftext: None,
1978                    attrlist: None,
1979                })
1980            );
1981
1982            assert!(blocks.next().is_none());
1983        }
1984    }
1985
1986    mod table {
1987        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1988
1989        #[test]
1990        fn empty() {
1991            let mut parser = Parser::default();
1992            assert!(
1993                crate::blocks::CompoundDelimitedBlock::parse(
1994                    &BlockMetadata::new("|===\n|==="),
1995                    &mut parser
1996                )
1997                .is_none()
1998            );
1999
2000            let mut parser = Parser::default();
2001            assert!(
2002                crate::blocks::CompoundDelimitedBlock::parse(
2003                    &BlockMetadata::new(",===\n,==="),
2004                    &mut parser
2005                )
2006                .is_none()
2007            );
2008
2009            let mut parser = Parser::default();
2010            assert!(
2011                crate::blocks::CompoundDelimitedBlock::parse(
2012                    &BlockMetadata::new(":===\n:==="),
2013                    &mut parser
2014                )
2015                .is_none()
2016            );
2017
2018            let mut parser = Parser::default();
2019            assert!(
2020                crate::blocks::CompoundDelimitedBlock::parse(
2021                    &BlockMetadata::new("!===\n!==="),
2022                    &mut parser
2023                )
2024                .is_none()
2025            );
2026        }
2027
2028        #[test]
2029        fn multiple_lines() {
2030            let mut parser = Parser::default();
2031            assert!(
2032                crate::blocks::CompoundDelimitedBlock::parse(
2033                    &BlockMetadata::new("|===\nline1  \nline2\n|==="),
2034                    &mut parser
2035                )
2036                .is_none()
2037            );
2038
2039            let mut parser = Parser::default();
2040            assert!(
2041                crate::blocks::CompoundDelimitedBlock::parse(
2042                    &BlockMetadata::new(",===\nline1  \nline2\n,==="),
2043                    &mut parser
2044                )
2045                .is_none()
2046            );
2047
2048            let mut parser = Parser::default();
2049            assert!(
2050                crate::blocks::CompoundDelimitedBlock::parse(
2051                    &BlockMetadata::new(":===\nline1  \nline2\n:==="),
2052                    &mut parser
2053                )
2054                .is_none()
2055            );
2056
2057            let mut parser = Parser::default();
2058            assert!(
2059                crate::blocks::CompoundDelimitedBlock::parse(
2060                    &BlockMetadata::new("!===\nline1  \nline2\n!==="),
2061                    &mut parser
2062                )
2063                .is_none()
2064            );
2065        }
2066    }
2067
2068    mod pass {
2069        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
2070
2071        #[test]
2072        fn empty() {
2073            let mut parser = Parser::default();
2074            assert!(
2075                crate::blocks::CompoundDelimitedBlock::parse(
2076                    &BlockMetadata::new("++++\n++++"),
2077                    &mut parser
2078                )
2079                .is_none()
2080            );
2081        }
2082
2083        #[test]
2084        fn multiple_lines() {
2085            let mut parser = Parser::default();
2086            assert!(
2087                crate::blocks::CompoundDelimitedBlock::parse(
2088                    &BlockMetadata::new("++++\nline1  \nline2\n++++"),
2089                    &mut parser
2090                )
2091                .is_none()
2092            );
2093        }
2094    }
2095
2096    #[test]
2097    fn impl_debug() {
2098        let mut parser = Parser::default();
2099
2100        let cdb = crate::blocks::CompoundDelimitedBlock::parse(
2101            &BlockMetadata::new("====\nblock1\n\nblock2\n===="),
2102            &mut parser,
2103        )
2104        .unwrap()
2105        .unwrap_if_no_warnings()
2106        .unwrap()
2107        .item;
2108
2109        assert_eq!(
2110            format!("{cdb:#?}"),
2111            r#"CompoundDelimitedBlock {
2112    blocks: &[
2113        Block::Simple(
2114            SimpleBlock {
2115                content: Content {
2116                    original: Span {
2117                        data: "block1",
2118                        line: 2,
2119                        col: 1,
2120                        offset: 5,
2121                    },
2122                    rendered: "block1",
2123                },
2124                source: Span {
2125                    data: "block1",
2126                    line: 2,
2127                    col: 1,
2128                    offset: 5,
2129                },
2130                style: SimpleBlockStyle::Paragraph,
2131                title_source: None,
2132                title: None,
2133                caption: None,
2134                number: None,
2135                anchor: None,
2136                anchor_reftext: None,
2137                attrlist: None,
2138            },
2139        ),
2140        Block::Simple(
2141            SimpleBlock {
2142                content: Content {
2143                    original: Span {
2144                        data: "block2",
2145                        line: 4,
2146                        col: 1,
2147                        offset: 13,
2148                    },
2149                    rendered: "block2",
2150                },
2151                source: Span {
2152                    data: "block2",
2153                    line: 4,
2154                    col: 1,
2155                    offset: 13,
2156                },
2157                style: SimpleBlockStyle::Paragraph,
2158                title_source: None,
2159                title: None,
2160                caption: None,
2161                number: None,
2162                anchor: None,
2163                anchor_reftext: None,
2164                attrlist: None,
2165            },
2166        ),
2167    ],
2168    context: "example",
2169    source: Span {
2170        data: "====\nblock1\n\nblock2\n====",
2171        line: 1,
2172        col: 1,
2173        offset: 0,
2174    },
2175    title_source: None,
2176    title: None,
2177    caption: None,
2178    number: None,
2179    anchor: None,
2180    anchor_reftext: None,
2181    attrlist: None,
2182}"#
2183        );
2184    }
2185}