Skip to main content

asciidoc_parser/blocks/
compound_delimited.rs

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