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