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