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