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