Skip to main content

asciidoc_parser/blocks/
raw_delimited.rs

1use crate::{
2    HasSpan, Parser, Span,
3    attributes::Attrlist,
4    blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
5    content::{Content, SubstitutionGroup},
6    span::MatchedItem,
7    strings::CowStr,
8    warnings::{MatchAndWarnings, Warning, WarningType},
9};
10
11/// A delimited block that contains verbatim, raw, or comment text. The content
12/// between the matching delimiters is not parsed for block syntax.
13///
14/// The following delimiters are recognized as raw delimited blocks:
15///
16/// | Delimiter | Content type |
17/// |-----------|--------------|
18/// | `////`    | Comment      |
19/// | `----`    | Listing      |
20/// | `....`    | Literal      |
21/// | `++++`    | Passthrough  |
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct RawDelimitedBlock<'src> {
24    content: Content<'src>,
25    content_model: ContentModel,
26    context: CowStr<'src>,
27    source: Span<'src>,
28    title_source: Option<Span<'src>>,
29    title: Option<String>,
30    anchor: Option<Span<'src>>,
31    anchor_reftext: Option<Span<'src>>,
32    attrlist: Option<Attrlist<'src>>,
33    substitution_group: SubstitutionGroup,
34}
35
36impl<'src> RawDelimitedBlock<'src> {
37    pub(crate) fn is_valid_delimiter(line: &Span<'src>) -> bool {
38        let data = line.data();
39
40        // TO DO (https://github.com/asciidoc-rs/asciidoc-parser/issues/145):
41        // Seek spec clarity: Do the characters after the fourth char
42        // have to match the first four?
43
44        if data.len() >= 4 {
45            if data.starts_with("////") {
46                data.split_at(4).1.chars().all(|c| c == '/')
47            } else if data.starts_with("----") {
48                data.split_at(4).1.chars().all(|c| c == '-')
49            } else if data.starts_with("....") {
50                data.split_at(4).1.chars().all(|c| c == '.')
51            } else if data.starts_with("++++") {
52                data.split_at(4).1.chars().all(|c| c == '+')
53            } else {
54                false
55            }
56        } else {
57            false
58        }
59    }
60
61    pub(crate) fn parse(
62        metadata: &BlockMetadata<'src>,
63        parser: &mut Parser,
64    ) -> Option<MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>>> {
65        let delimiter = metadata.block_start.take_normalized_line();
66
67        if delimiter.item.len() < 4 {
68            return None;
69        }
70
71        let (content_model, context, mut substitution_group) = match delimiter
72            .item
73            .data()
74            .split_at_checked(delimiter.item.data().len().min(4))?
75            .0
76        {
77            "////" => (ContentModel::Raw, "comment", SubstitutionGroup::None),
78            "----" => (
79                ContentModel::Verbatim,
80                "listing",
81                SubstitutionGroup::Verbatim,
82            ),
83            "...." => (
84                ContentModel::Verbatim,
85                "literal",
86                SubstitutionGroup::Verbatim,
87            ),
88            "++++" => (ContentModel::Raw, "pass", SubstitutionGroup::Pass),
89            _ => {
90                return None;
91            }
92        };
93
94        if !Self::is_valid_delimiter(&delimiter.item) {
95            return None;
96        }
97
98        let content_start = delimiter.after;
99        let mut next = content_start;
100
101        while !next.is_empty() {
102            let line = next.take_normalized_line();
103            if line.item.data() == delimiter.item.data() {
104                let content = content_start.trim_remainder(next).trim_trailing_line_end();
105
106                let mut content: Content<'src> = content.into();
107
108                substitution_group =
109                    substitution_group.override_via_attrlist(metadata.attrlist.as_ref());
110
111                substitution_group.apply(&mut content, parser, metadata.attrlist.as_ref());
112
113                return Some(MatchAndWarnings {
114                    item: Some(MatchedItem {
115                        item: Self {
116                            content,
117                            content_model,
118                            context: context.into(),
119                            source: metadata
120                                .source
121                                .trim_remainder(line.after)
122                                .trim_trailing_line_end(),
123                            title_source: metadata.title_source,
124                            title: metadata.title.clone(),
125                            anchor: metadata.anchor,
126                            anchor_reftext: metadata.anchor_reftext,
127                            attrlist: metadata.attrlist.clone(),
128                            substitution_group,
129                        },
130                        after: line.after,
131                    }),
132                    warnings: vec![],
133                });
134            }
135
136            next = line.after;
137        }
138
139        let content = content_start.trim_remainder(next).trim_trailing_line_end();
140
141        Some(MatchAndWarnings {
142            item: Some(MatchedItem {
143                item: Self {
144                    content: content.into(),
145                    content_model,
146                    context: context.into(),
147                    source: metadata
148                        .source
149                        .trim_remainder(next)
150                        .trim_trailing_line_end(),
151                    title_source: metadata.title_source,
152                    title: metadata.title.clone(),
153                    anchor: metadata.anchor,
154                    anchor_reftext: metadata.anchor_reftext,
155                    attrlist: metadata.attrlist.clone(),
156                    substitution_group,
157                },
158                after: next,
159            }),
160            warnings: vec![Warning {
161                source: delimiter.item,
162                warning: WarningType::UnterminatedDelimitedBlock,
163            }],
164        })
165    }
166
167    /// Return the interpreted content of this block.
168    pub fn content(&self) -> &Content<'src> {
169        &self.content
170    }
171}
172
173impl<'src> IsBlock<'src> for RawDelimitedBlock<'src> {
174    fn content_model(&self) -> ContentModel {
175        self.content_model
176    }
177
178    fn content_mut(&mut self) -> Option<&mut Content<'src>> {
179        Some(&mut self.content)
180    }
181
182    fn rendered_content(&self) -> Option<&str> {
183        Some(self.content.rendered())
184    }
185
186    fn raw_context(&self) -> CowStr<'src> {
187        self.context.clone()
188    }
189
190    fn title_source(&'src self) -> Option<Span<'src>> {
191        self.title_source
192    }
193
194    fn title(&self) -> Option<&str> {
195        self.title.as_deref()
196    }
197
198    fn anchor(&'src self) -> Option<Span<'src>> {
199        self.anchor
200    }
201
202    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
203        self.anchor_reftext
204    }
205
206    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
207        self.attrlist.as_ref()
208    }
209
210    fn substitution_group(&'src self) -> SubstitutionGroup {
211        self.substitution_group.clone()
212    }
213}
214
215impl<'src> HasSpan<'src> for RawDelimitedBlock<'src> {
216    fn span(&self) -> Span<'src> {
217        self.source
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    #![allow(clippy::unwrap_used)]
224
225    mod is_valid_delimiter {
226        use crate::blocks::RawDelimitedBlock;
227
228        #[test]
229        fn comment() {
230            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
231                "////"
232            )));
233            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
234                "/////"
235            )));
236            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
237                "/////////"
238            )));
239
240            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
241                "///"
242            )));
243            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
244                "//-/"
245            )));
246            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
247                "////-"
248            )));
249            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
250                "//////////x"
251            )));
252        }
253
254        #[test]
255        fn example() {
256            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
257                "===="
258            )));
259            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
260                "====="
261            )));
262            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
263                "==="
264            )));
265        }
266
267        #[test]
268        fn listing() {
269            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
270                "----"
271            )));
272            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
273                "-----"
274            )));
275            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
276                "---------"
277            )));
278
279            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
280                "---"
281            )));
282            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
283                "--/-"
284            )));
285            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
286                "----/"
287            )));
288            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
289                "----------x"
290            )));
291        }
292
293        #[test]
294        fn literal() {
295            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
296                "...."
297            )));
298            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
299                "....."
300            )));
301            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
302                "........."
303            )));
304
305            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
306                "..."
307            )));
308            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
309                "../."
310            )));
311            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
312                "..../"
313            )));
314            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
315                "..........x"
316            )));
317        }
318
319        #[test]
320        fn sidebar() {
321            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
322                "****"
323            )));
324            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
325                "*****"
326            )));
327            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
328                "***"
329            )));
330        }
331
332        #[test]
333        fn table() {
334            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
335                "|==="
336            )));
337            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
338                ",==="
339            )));
340            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
341                ":==="
342            )));
343            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
344                "!==="
345            )));
346        }
347
348        #[test]
349        fn pass() {
350            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
351                "++++"
352            )));
353            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
354                "+++++"
355            )));
356            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
357                "+++++++++"
358            )));
359
360            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
361                "+++"
362            )));
363            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
364                "++/+"
365            )));
366            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
367                "++++/"
368            )));
369            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
370                "++++++++++x"
371            )));
372        }
373
374        #[test]
375        fn quote() {
376            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
377                "____"
378            )));
379            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
380                "_____"
381            )));
382            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
383                "___"
384            )));
385        }
386    }
387
388    mod parse {
389        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
390
391        #[test]
392        fn err_invalid_delimiter() {
393            let mut parser = Parser::default();
394            assert!(
395                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new(""), &mut parser)
396                    .is_none()
397            );
398
399            let mut parser = Parser::default();
400            assert!(
401                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("..."), &mut parser)
402                    .is_none()
403            );
404
405            let mut parser = Parser::default();
406            assert!(
407                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("++++x"), &mut parser)
408                    .is_none()
409            );
410
411            let mut parser = Parser::default();
412            assert!(
413                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("____x"), &mut parser)
414                    .is_none()
415            );
416
417            let mut parser = Parser::default();
418            assert!(
419                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("====x"), &mut parser)
420                    .is_none()
421            );
422
423            let mut parser = Parser::default();
424            assert!(
425                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("==\n=="), &mut parser)
426                    .is_none()
427            );
428
429            let mut parser = Parser::default();
430            assert!(
431                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("===😀"), &mut parser)
432                    .is_none()
433            );
434        }
435
436        #[test]
437        fn err_unterminated() {
438            let mut parser = Parser::default();
439
440            let maw = crate::blocks::RawDelimitedBlock::parse(
441                &BlockMetadata::new("....\nblah blah blah"),
442                &mut parser,
443            )
444            .unwrap();
445
446            assert_eq!(
447                maw.warnings,
448                vec![Warning {
449                    source: Span {
450                        data: "....",
451                        line: 1,
452                        col: 1,
453                        offset: 0,
454                    },
455                    warning: WarningType::UnterminatedDelimitedBlock,
456                }]
457            );
458        }
459    }
460
461    mod comment {
462        use crate::{
463            blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
464            tests::prelude::*,
465        };
466
467        #[test]
468        fn empty() {
469            let mut parser = Parser::default();
470            let maw = crate::blocks::RawDelimitedBlock::parse(
471                &BlockMetadata::new("////\n////"),
472                &mut parser,
473            )
474            .unwrap();
475
476            let mi = maw.item.unwrap().clone();
477
478            assert_eq!(
479                mi.item,
480                RawDelimitedBlock {
481                    content: Content {
482                        original: Span {
483                            data: "",
484                            line: 2,
485                            col: 1,
486                            offset: 5,
487                        },
488                        rendered: "",
489                    },
490                    content_model: ContentModel::Raw,
491                    context: "comment",
492                    source: Span {
493                        data: "////\n////",
494                        line: 1,
495                        col: 1,
496                        offset: 0,
497                    },
498                    title_source: None,
499                    title: None,
500                    anchor: None,
501                    anchor_reftext: None,
502                    attrlist: None,
503                    substitution_group: SubstitutionGroup::None,
504                }
505            );
506
507            assert_eq!(mi.item.content_model(), ContentModel::Raw);
508            assert_eq!(mi.item.rendered_content().unwrap(), "");
509            assert_eq!(mi.item.raw_context().as_ref(), "comment");
510            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
511            assert!(mi.item.declared_style().is_none());
512            assert!(mi.item.content().is_empty());
513            assert!(mi.item.id().is_none());
514            assert!(mi.item.roles().is_empty());
515            assert!(mi.item.options().is_empty());
516            assert!(mi.item.title_source().is_none());
517            assert!(mi.item.title().is_none());
518            assert!(mi.item.anchor().is_none());
519            assert!(mi.item.anchor_reftext().is_none());
520            assert!(mi.item.attrlist().is_none());
521            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
522        }
523
524        #[test]
525        fn multiple_lines() {
526            let mut parser = Parser::default();
527
528            let maw = crate::blocks::RawDelimitedBlock::parse(
529                &BlockMetadata::new("////\nline1  \nline2\n////"),
530                &mut parser,
531            )
532            .unwrap();
533
534            let mi = maw.item.unwrap().clone();
535
536            assert_eq!(
537                mi.item,
538                RawDelimitedBlock {
539                    content: Content {
540                        original: Span {
541                            data: "line1  \nline2",
542                            line: 2,
543                            col: 1,
544                            offset: 5,
545                        },
546                        rendered: "line1  \nline2",
547                    },
548                    content_model: ContentModel::Raw,
549                    context: "comment",
550                    source: Span {
551                        data: "////\nline1  \nline2\n////",
552                        line: 1,
553                        col: 1,
554                        offset: 0,
555                    },
556                    title_source: None,
557                    title: None,
558                    anchor: None,
559                    anchor_reftext: None,
560                    attrlist: None,
561                    substitution_group: SubstitutionGroup::None,
562                }
563            );
564
565            assert_eq!(mi.item.content_model(), ContentModel::Raw);
566            assert_eq!(mi.item.rendered_content().unwrap(), "line1  \nline2");
567            assert_eq!(mi.item.raw_context().as_ref(), "comment");
568            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
569            assert!(mi.item.declared_style().is_none());
570            assert!(mi.item.id().is_none());
571            assert!(mi.item.roles().is_empty());
572            assert!(mi.item.options().is_empty());
573            assert!(mi.item.title_source().is_none());
574            assert!(mi.item.title().is_none());
575            assert!(mi.item.anchor().is_none());
576            assert!(mi.item.anchor_reftext().is_none());
577            assert!(mi.item.attrlist().is_none());
578            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
579
580            assert_eq!(
581                mi.item.content(),
582                Content {
583                    original: Span {
584                        data: "line1  \nline2",
585                        line: 2,
586                        col: 1,
587                        offset: 5,
588                    },
589                    rendered: "line1  \nline2",
590                }
591            );
592        }
593
594        #[test]
595        fn ignores_delimiter_prefix() {
596            let mut parser = Parser::default();
597
598            let maw = crate::blocks::RawDelimitedBlock::parse(
599                &BlockMetadata::new("////\nline1  \n/////\nline2\n////"),
600                &mut parser,
601            )
602            .unwrap();
603
604            let mi = maw.item.unwrap().clone();
605
606            assert_eq!(
607                mi.item,
608                RawDelimitedBlock {
609                    content: Content {
610                        original: Span {
611                            data: "line1  \n/////\nline2",
612                            line: 2,
613                            col: 1,
614                            offset: 5,
615                        },
616                        rendered: "line1  \n/////\nline2",
617                    },
618                    content_model: ContentModel::Raw,
619                    context: "comment",
620                    source: Span {
621                        data: "////\nline1  \n/////\nline2\n////",
622                        line: 1,
623                        col: 1,
624                        offset: 0,
625                    },
626                    title_source: None,
627                    title: None,
628                    anchor: None,
629                    anchor_reftext: None,
630                    attrlist: None,
631                    substitution_group: SubstitutionGroup::None,
632                }
633            );
634
635            assert_eq!(mi.item.content_model(), ContentModel::Raw);
636            assert_eq!(mi.item.raw_context().as_ref(), "comment");
637            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
638            assert!(mi.item.declared_style().is_none());
639            assert!(mi.item.id().is_none());
640            assert!(mi.item.roles().is_empty());
641            assert!(mi.item.options().is_empty());
642            assert!(mi.item.title_source().is_none());
643            assert!(mi.item.title().is_none());
644            assert!(mi.item.anchor().is_none());
645            assert!(mi.item.anchor_reftext().is_none());
646            assert!(mi.item.attrlist().is_none());
647            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
648
649            assert_eq!(
650                mi.item.content(),
651                Content {
652                    original: Span {
653                        data: "line1  \n/////\nline2",
654                        line: 2,
655                        col: 1,
656                        offset: 5,
657                    },
658                    rendered: "line1  \n/////\nline2",
659                }
660            );
661        }
662
663        #[test]
664        fn no_panic_for_utf8_code_point_using_more_than_one_byte() {
665            let mut parser = Parser::default();
666            assert!(
667                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("///😀"), &mut parser)
668                    .is_none()
669            );
670        }
671    }
672
673    mod example {
674        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
675
676        #[test]
677        fn empty() {
678            let mut parser = Parser::default();
679            assert!(
680                crate::blocks::RawDelimitedBlock::parse(
681                    &BlockMetadata::new("====\n===="),
682                    &mut parser
683                )
684                .is_none()
685            );
686        }
687
688        #[test]
689        fn multiple_lines() {
690            let mut parser = Parser::default();
691            assert!(
692                crate::blocks::RawDelimitedBlock::parse(
693                    &BlockMetadata::new("====\nline1  \nline2\n===="),
694                    &mut parser
695                )
696                .is_none()
697            );
698        }
699    }
700
701    mod listing {
702        use crate::{
703            blocks::{ContentModel, metadata::BlockMetadata},
704            content::SubstitutionStep,
705            tests::prelude::*,
706        };
707
708        #[test]
709        fn empty() {
710            let mut parser = Parser::default();
711
712            let maw = crate::blocks::RawDelimitedBlock::parse(
713                &BlockMetadata::new("----\n----"),
714                &mut parser,
715            )
716            .unwrap();
717
718            let mi = maw.item.unwrap().clone();
719
720            assert_eq!(
721                mi.item,
722                RawDelimitedBlock {
723                    content: Content {
724                        original: Span {
725                            data: "",
726                            line: 2,
727                            col: 1,
728                            offset: 5,
729                        },
730                        rendered: "",
731                    },
732                    content_model: ContentModel::Verbatim,
733                    context: "listing",
734                    source: Span {
735                        data: "----\n----",
736                        line: 1,
737                        col: 1,
738                        offset: 0,
739                    },
740                    title_source: None,
741                    title: None,
742                    anchor: None,
743                    anchor_reftext: None,
744                    attrlist: None,
745                    substitution_group: SubstitutionGroup::Verbatim,
746                }
747            );
748
749            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
750            assert_eq!(mi.item.raw_context().as_ref(), "listing");
751            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
752            assert!(mi.item.declared_style().is_none());
753            assert!(mi.item.content().is_empty());
754            assert!(mi.item.id().is_none());
755            assert!(mi.item.roles().is_empty());
756            assert!(mi.item.options().is_empty());
757            assert!(mi.item.title_source().is_none());
758            assert!(mi.item.title().is_none());
759            assert!(mi.item.anchor().is_none());
760            assert!(mi.item.anchor_reftext().is_none());
761            assert!(mi.item.attrlist().is_none());
762            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
763        }
764
765        #[test]
766        fn multiple_lines() {
767            let mut parser = Parser::default();
768
769            let maw = crate::blocks::RawDelimitedBlock::parse(
770                &BlockMetadata::new("----\nline1  \nline2\n----"),
771                &mut parser,
772            )
773            .unwrap();
774
775            let mi = maw.item.unwrap().clone();
776
777            assert_eq!(
778                mi.item,
779                RawDelimitedBlock {
780                    content: Content {
781                        original: Span {
782                            data: "line1  \nline2",
783                            line: 2,
784                            col: 1,
785                            offset: 5,
786                        },
787                        rendered: "line1  \nline2",
788                    },
789                    content_model: ContentModel::Verbatim,
790                    context: "listing",
791                    source: Span {
792                        data: "----\nline1  \nline2\n----",
793                        line: 1,
794                        col: 1,
795                        offset: 0,
796                    },
797                    title_source: None,
798                    title: None,
799                    anchor: None,
800                    anchor_reftext: None,
801                    attrlist: None,
802                    substitution_group: SubstitutionGroup::Verbatim,
803                }
804            );
805
806            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
807            assert_eq!(mi.item.raw_context().as_ref(), "listing");
808            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
809            assert!(mi.item.declared_style().is_none());
810            assert!(mi.item.id().is_none());
811            assert!(mi.item.roles().is_empty());
812            assert!(mi.item.options().is_empty());
813            assert!(mi.item.title_source().is_none());
814            assert!(mi.item.title().is_none());
815            assert!(mi.item.anchor().is_none());
816            assert!(mi.item.anchor_reftext().is_none());
817            assert!(mi.item.attrlist().is_none());
818            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
819
820            assert_eq!(
821                mi.item.content(),
822                Content {
823                    original: Span {
824                        data: "line1  \nline2",
825                        line: 2,
826                        col: 1,
827                        offset: 5,
828                    },
829                    rendered: "line1  \nline2",
830                }
831            );
832        }
833
834        #[test]
835        fn overrides_sub_group_via_subs_attribute() {
836            let mut parser = Parser::default();
837
838            let maw = crate::blocks::RawDelimitedBlock::parse(
839                &BlockMetadata::new("[subs=quotes]\n----\nline1 < *line2*\n----"),
840                &mut parser,
841            )
842            .unwrap();
843
844            let mi = maw.item.unwrap().clone();
845
846            assert_eq!(
847                mi.item,
848                RawDelimitedBlock {
849                    content: Content {
850                        original: Span {
851                            data: "line1 < *line2*",
852                            line: 3,
853                            col: 1,
854                            offset: 19,
855                        },
856                        rendered: "line1 < <strong>line2</strong>",
857                    },
858                    content_model: ContentModel::Verbatim,
859                    context: "listing",
860                    source: Span {
861                        data: "[subs=quotes]\n----\nline1 < *line2*\n----",
862                        line: 1,
863                        col: 1,
864                        offset: 0,
865                    },
866                    title_source: None,
867                    title: None,
868                    anchor: None,
869                    anchor_reftext: None,
870                    attrlist: Some(Attrlist {
871                        attributes: &[ElementAttribute {
872                            name: Some("subs"),
873                            value: "quotes",
874                            shorthand_items: &[],
875                        },],
876                        anchor: None,
877                        source: Span {
878                            data: "subs=quotes",
879                            line: 1,
880                            col: 2,
881                            offset: 1,
882                        },
883                    },),
884                    substitution_group: SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
885                }
886            );
887
888            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
889            assert_eq!(mi.item.raw_context().as_ref(), "listing");
890            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
891            assert!(mi.item.declared_style().is_none());
892            assert!(mi.item.id().is_none());
893            assert!(mi.item.roles().is_empty());
894            assert!(mi.item.options().is_empty());
895            assert!(mi.item.title_source().is_none());
896            assert!(mi.item.title().is_none());
897            assert!(mi.item.anchor().is_none());
898            assert!(mi.item.anchor_reftext().is_none());
899
900            assert_eq!(
901                mi.item.attrlist().unwrap(),
902                Attrlist {
903                    attributes: &[ElementAttribute {
904                        name: Some("subs"),
905                        value: "quotes",
906                        shorthand_items: &[],
907                    },],
908                    anchor: None,
909                    source: Span {
910                        data: "subs=quotes",
911                        line: 1,
912                        col: 2,
913                        offset: 1,
914                    },
915                }
916            );
917
918            assert_eq!(
919                mi.item.substitution_group(),
920                SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes])
921            );
922
923            assert_eq!(
924                mi.item.content(),
925                Content {
926                    original: Span {
927                        data: "line1 < *line2*",
928                        line: 3,
929                        col: 1,
930                        offset: 19,
931                    },
932                    rendered: "line1 < <strong>line2</strong>",
933                }
934            );
935        }
936
937        #[test]
938        fn ignores_delimiter_prefix() {
939            let mut parser = Parser::default();
940
941            let maw = crate::blocks::RawDelimitedBlock::parse(
942                &BlockMetadata::new("----\nline1  \n-----\nline2\n----"),
943                &mut parser,
944            )
945            .unwrap();
946
947            let mi = maw.item.unwrap().clone();
948
949            assert_eq!(
950                mi.item,
951                RawDelimitedBlock {
952                    content: Content {
953                        original: Span {
954                            data: "line1  \n-----\nline2",
955                            line: 2,
956                            col: 1,
957                            offset: 5,
958                        },
959                        rendered: "line1  \n-----\nline2",
960                    },
961                    content_model: ContentModel::Verbatim,
962                    context: "listing",
963                    source: Span {
964                        data: "----\nline1  \n-----\nline2\n----",
965                        line: 1,
966                        col: 1,
967                        offset: 0,
968                    },
969                    title_source: None,
970                    title: None,
971                    anchor: None,
972                    anchor_reftext: None,
973                    attrlist: None,
974                    substitution_group: SubstitutionGroup::Verbatim,
975                }
976            );
977
978            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
979            assert_eq!(mi.item.raw_context().as_ref(), "listing");
980            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
981            assert!(mi.item.declared_style().is_none());
982            assert!(mi.item.id().is_none());
983            assert!(mi.item.roles().is_empty());
984            assert!(mi.item.options().is_empty());
985            assert!(mi.item.title_source().is_none());
986            assert!(mi.item.title().is_none());
987            assert!(mi.item.anchor().is_none());
988            assert!(mi.item.anchor_reftext().is_none());
989            assert!(mi.item.attrlist().is_none());
990            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
991
992            assert_eq!(
993                mi.item.content(),
994                Content {
995                    original: Span {
996                        data: "line1  \n-----\nline2",
997                        line: 2,
998                        col: 1,
999                        offset: 5,
1000                    },
1001                    rendered: "line1  \n-----\nline2",
1002                }
1003            );
1004
1005            assert_eq!(
1006                mi.item.content(),
1007                Content {
1008                    original: Span {
1009                        data: "line1  \n-----\nline2",
1010                        line: 2,
1011                        col: 1,
1012                        offset: 5,
1013                    },
1014                    rendered: "line1  \n-----\nline2",
1015                }
1016            );
1017        }
1018    }
1019
1020    mod sidebar {
1021        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1022
1023        #[test]
1024        fn empty() {
1025            let mut parser = Parser::default();
1026            assert!(
1027                crate::blocks::RawDelimitedBlock::parse(
1028                    &BlockMetadata::new("****\n****"),
1029                    &mut parser
1030                )
1031                .is_none()
1032            );
1033        }
1034
1035        #[test]
1036        fn multiple_lines() {
1037            let mut parser = Parser::default();
1038            assert!(
1039                crate::blocks::RawDelimitedBlock::parse(
1040                    &BlockMetadata::new("****\nline1  \nline2\n****"),
1041                    &mut parser
1042                )
1043                .is_none()
1044            );
1045        }
1046    }
1047
1048    mod table {
1049        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1050
1051        #[test]
1052        fn empty() {
1053            let mut parser = Parser::default();
1054            assert!(
1055                crate::blocks::RawDelimitedBlock::parse(
1056                    &BlockMetadata::new("|===\n|==="),
1057                    &mut parser
1058                )
1059                .is_none()
1060            );
1061
1062            let mut parser = Parser::default();
1063            assert!(
1064                crate::blocks::RawDelimitedBlock::parse(
1065                    &BlockMetadata::new(",===\n,==="),
1066                    &mut parser
1067                )
1068                .is_none()
1069            );
1070
1071            let mut parser = Parser::default();
1072            assert!(
1073                crate::blocks::RawDelimitedBlock::parse(
1074                    &BlockMetadata::new(":===\n:==="),
1075                    &mut parser
1076                )
1077                .is_none()
1078            );
1079
1080            let mut parser = Parser::default();
1081            assert!(
1082                crate::blocks::RawDelimitedBlock::parse(
1083                    &BlockMetadata::new("!===\n!==="),
1084                    &mut parser
1085                )
1086                .is_none()
1087            );
1088        }
1089
1090        #[test]
1091        fn multiple_lines() {
1092            let mut parser = Parser::default();
1093            assert!(
1094                crate::blocks::RawDelimitedBlock::parse(
1095                    &BlockMetadata::new("|===\nline1  \nline2\n|==="),
1096                    &mut parser
1097                )
1098                .is_none()
1099            );
1100
1101            let mut parser = Parser::default();
1102            assert!(
1103                crate::blocks::RawDelimitedBlock::parse(
1104                    &BlockMetadata::new(",===\nline1  \nline2\n,==="),
1105                    &mut parser
1106                )
1107                .is_none()
1108            );
1109
1110            let mut parser = Parser::default();
1111            assert!(
1112                crate::blocks::RawDelimitedBlock::parse(
1113                    &BlockMetadata::new(":===\nline1  \nline2\n:==="),
1114                    &mut parser
1115                )
1116                .is_none()
1117            );
1118
1119            let mut parser = Parser::default();
1120            assert!(
1121                crate::blocks::RawDelimitedBlock::parse(
1122                    &BlockMetadata::new("!===\nline1  \nline2\n!==="),
1123                    &mut parser
1124                )
1125                .is_none()
1126            );
1127        }
1128    }
1129
1130    mod pass {
1131        use crate::{
1132            blocks::{ContentModel, metadata::BlockMetadata},
1133            tests::prelude::*,
1134        };
1135
1136        #[test]
1137        fn empty() {
1138            let mut parser = Parser::default();
1139            let maw = crate::blocks::RawDelimitedBlock::parse(
1140                &BlockMetadata::new("++++\n++++"),
1141                &mut parser,
1142            )
1143            .unwrap();
1144
1145            let mi = maw.item.unwrap().clone();
1146
1147            assert_eq!(
1148                mi.item,
1149                RawDelimitedBlock {
1150                    content: Content {
1151                        original: Span {
1152                            data: "",
1153                            line: 2,
1154                            col: 1,
1155                            offset: 5,
1156                        },
1157                        rendered: "",
1158                    },
1159                    content_model: ContentModel::Raw,
1160                    context: "pass",
1161                    source: Span {
1162                        data: "++++\n++++",
1163                        line: 1,
1164                        col: 1,
1165                        offset: 0,
1166                    },
1167                    title_source: None,
1168                    title: None,
1169                    anchor: None,
1170                    anchor_reftext: None,
1171                    attrlist: None,
1172                    substitution_group: SubstitutionGroup::Pass,
1173                }
1174            );
1175
1176            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1177            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1178            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1179            assert!(mi.item.declared_style().is_none());
1180            assert!(mi.item.content().is_empty());
1181            assert!(mi.item.id().is_none());
1182            assert!(mi.item.roles().is_empty());
1183            assert!(mi.item.options().is_empty());
1184            assert!(mi.item.title_source().is_none());
1185            assert!(mi.item.title().is_none());
1186            assert!(mi.item.anchor().is_none());
1187            assert!(mi.item.anchor_reftext().is_none());
1188            assert!(mi.item.attrlist().is_none());
1189            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1190        }
1191
1192        #[test]
1193        fn multiple_lines() {
1194            let mut parser = Parser::default();
1195
1196            let maw = crate::blocks::RawDelimitedBlock::parse(
1197                &BlockMetadata::new("++++\nline1  \nline2\n++++"),
1198                &mut parser,
1199            )
1200            .unwrap();
1201
1202            let mi = maw.item.unwrap().clone();
1203
1204            assert_eq!(
1205                mi.item,
1206                RawDelimitedBlock {
1207                    content: Content {
1208                        original: Span {
1209                            data: "line1  \nline2",
1210                            line: 2,
1211                            col: 1,
1212                            offset: 5,
1213                        },
1214                        rendered: "line1  \nline2",
1215                    },
1216                    content_model: ContentModel::Raw,
1217                    context: "pass",
1218                    source: Span {
1219                        data: "++++\nline1  \nline2\n++++",
1220                        line: 1,
1221                        col: 1,
1222                        offset: 0,
1223                    },
1224                    title_source: None,
1225                    title: None,
1226                    anchor: None,
1227                    anchor_reftext: None,
1228                    attrlist: None,
1229                    substitution_group: SubstitutionGroup::Pass,
1230                }
1231            );
1232
1233            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1234            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1235            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1236            assert!(mi.item.declared_style().is_none());
1237            assert!(mi.item.id().is_none());
1238            assert!(mi.item.roles().is_empty());
1239            assert!(mi.item.options().is_empty());
1240            assert!(mi.item.title_source().is_none());
1241            assert!(mi.item.title().is_none());
1242            assert!(mi.item.anchor().is_none());
1243            assert!(mi.item.anchor_reftext().is_none());
1244            assert!(mi.item.attrlist().is_none());
1245            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1246
1247            assert_eq!(
1248                mi.item.content(),
1249                Content {
1250                    original: Span {
1251                        data: "line1  \nline2",
1252                        line: 2,
1253                        col: 1,
1254                        offset: 5,
1255                    },
1256                    rendered: "line1  \nline2",
1257                }
1258            );
1259        }
1260
1261        #[test]
1262        fn ignores_delimiter_prefix() {
1263            let mut parser = Parser::default();
1264
1265            let maw = crate::blocks::RawDelimitedBlock::parse(
1266                &BlockMetadata::new("++++\nline1  \n+++++\nline2\n++++"),
1267                &mut parser,
1268            )
1269            .unwrap();
1270
1271            let mi = maw.item.unwrap().clone();
1272
1273            assert_eq!(
1274                mi.item,
1275                RawDelimitedBlock {
1276                    content: Content {
1277                        original: Span {
1278                            data: "line1  \n+++++\nline2",
1279                            line: 2,
1280                            col: 1,
1281                            offset: 5,
1282                        },
1283                        rendered: "line1  \n+++++\nline2",
1284                    },
1285                    content_model: ContentModel::Raw,
1286                    context: "pass",
1287                    source: Span {
1288                        data: "++++\nline1  \n+++++\nline2\n++++",
1289                        line: 1,
1290                        col: 1,
1291                        offset: 0,
1292                    },
1293                    title_source: None,
1294                    title: None,
1295                    anchor: None,
1296                    anchor_reftext: None,
1297                    attrlist: None,
1298                    substitution_group: SubstitutionGroup::Pass,
1299                }
1300            );
1301
1302            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1303            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1304            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1305            assert!(mi.item.declared_style().is_none());
1306            assert!(mi.item.id().is_none());
1307            assert!(mi.item.roles().is_empty());
1308            assert!(mi.item.options().is_empty());
1309            assert!(mi.item.title_source().is_none());
1310            assert!(mi.item.title().is_none());
1311            assert!(mi.item.anchor().is_none());
1312            assert!(mi.item.anchor_reftext().is_none());
1313            assert!(mi.item.attrlist().is_none());
1314            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1315
1316            assert_eq!(
1317                mi.item.content(),
1318                Content {
1319                    original: Span {
1320                        data: "line1  \n+++++\nline2",
1321                        line: 2,
1322                        col: 1,
1323                        offset: 5,
1324                    },
1325                    rendered: "line1  \n+++++\nline2",
1326                }
1327            );
1328        }
1329    }
1330
1331    mod quote {
1332        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1333
1334        #[test]
1335        fn empty() {
1336            let mut parser = Parser::default();
1337            assert!(
1338                crate::blocks::RawDelimitedBlock::parse(
1339                    &BlockMetadata::new("____\n____"),
1340                    &mut parser
1341                )
1342                .is_none()
1343            );
1344        }
1345
1346        #[test]
1347        fn multiple_lines() {
1348            let mut parser = Parser::default();
1349            assert!(
1350                crate::blocks::RawDelimitedBlock::parse(
1351                    &BlockMetadata::new("____\nline1  \nline2\n____"),
1352                    &mut parser
1353                )
1354                .is_none()
1355            );
1356        }
1357    }
1358}