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