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