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                "++++" => pass_or_stem_block_type(metadata.attrlist.as_ref()),
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
220/// Resolve the content model, context, and substitution group for a passthrough
221/// (`++++`) delimited block.
222///
223/// A passthrough block normally has the `pass` context and applies no
224/// substitutions. When it carries a STEM style (`stem`, `asciimath`, or
225/// `latexmath`), it instead becomes a `stem` block: a raw block whose
226/// expression has only the special characters substitution applied (the
227/// notation's math delimiters are added by the converter at render time).
228fn pass_or_stem_block_type(
229    attrlist: Option<&Attrlist<'_>>,
230) -> (ContentModel, &'static str, SubstitutionGroup) {
231    match attrlist.and_then(|a| a.block_style()) {
232        Some("stem") | Some("asciimath") | Some("latexmath") => {
233            (ContentModel::Raw, "stem", SubstitutionGroup::Stem)
234        }
235        _ => (ContentModel::Raw, "pass", SubstitutionGroup::Pass),
236    }
237}
238
239impl<'src> IsBlock<'src> for RawDelimitedBlock<'src> {
240    fn content_model(&self) -> ContentModel {
241        self.content_model
242    }
243
244    fn content_mut(&mut self) -> Option<&mut Content<'src>> {
245        Some(&mut self.content)
246    }
247
248    fn rendered_content(&self) -> Option<&str> {
249        Some(self.content.rendered())
250    }
251
252    fn raw_context(&self) -> CowStr<'src> {
253        self.context.clone()
254    }
255
256    fn title_source(&'src self) -> Option<Span<'src>> {
257        self.title_source
258    }
259
260    fn title(&self) -> Option<&str> {
261        self.title.as_deref()
262    }
263
264    fn anchor(&'src self) -> Option<Span<'src>> {
265        self.anchor
266    }
267
268    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
269        self.anchor_reftext
270    }
271
272    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
273        self.attrlist.as_ref()
274    }
275
276    fn substitution_group(&'src self) -> SubstitutionGroup {
277        self.substitution_group.clone()
278    }
279}
280
281impl<'src> HasSpan<'src> for RawDelimitedBlock<'src> {
282    fn span(&self) -> Span<'src> {
283        self.source
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    #![allow(clippy::unwrap_used)]
290
291    mod is_valid_delimiter {
292        use crate::blocks::RawDelimitedBlock;
293
294        #[test]
295        fn comment() {
296            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
297                "////"
298            )));
299            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
300                "/////"
301            )));
302            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
303                "/////////"
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            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
313                "////-"
314            )));
315            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
316                "//////////x"
317            )));
318        }
319
320        #[test]
321        fn example() {
322            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
323                "===="
324            )));
325            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
326                "====="
327            )));
328            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
329                "==="
330            )));
331        }
332
333        #[test]
334        fn listing() {
335            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
336                "----"
337            )));
338            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
339                "-----"
340            )));
341            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
342                "---------"
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            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
352                "----/"
353            )));
354            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
355                "----------x"
356            )));
357        }
358
359        #[test]
360        fn literal() {
361            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
362                "...."
363            )));
364            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
365                "....."
366            )));
367            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
368                "........."
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            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
378                "..../"
379            )));
380            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
381                "..........x"
382            )));
383        }
384
385        #[test]
386        fn sidebar() {
387            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
388                "****"
389            )));
390            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
391                "*****"
392            )));
393            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
394                "***"
395            )));
396        }
397
398        #[test]
399        fn table() {
400            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
401                "|==="
402            )));
403            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
404                ",==="
405            )));
406            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
407                ":==="
408            )));
409            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
410                "!==="
411            )));
412        }
413
414        #[test]
415        fn pass() {
416            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
417                "++++"
418            )));
419            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
420                "+++++"
421            )));
422            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
423                "+++++++++"
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            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
433                "++++/"
434            )));
435            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
436                "++++++++++x"
437            )));
438        }
439
440        #[test]
441        fn quote() {
442            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
443                "____"
444            )));
445            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
446                "_____"
447            )));
448            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
449                "___"
450            )));
451        }
452    }
453
454    mod parse {
455        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
456
457        #[test]
458        fn err_invalid_delimiter() {
459            let mut parser = Parser::default();
460            assert!(
461                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new(""), &mut parser)
462                    .is_none()
463            );
464
465            let mut parser = Parser::default();
466            assert!(
467                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("..."), &mut parser)
468                    .is_none()
469            );
470
471            let mut parser = Parser::default();
472            assert!(
473                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("++++x"), &mut parser)
474                    .is_none()
475            );
476
477            let mut parser = Parser::default();
478            assert!(
479                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("____x"), &mut parser)
480                    .is_none()
481            );
482
483            let mut parser = Parser::default();
484            assert!(
485                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("====x"), &mut parser)
486                    .is_none()
487            );
488
489            let mut parser = Parser::default();
490            assert!(
491                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("==\n=="), &mut parser)
492                    .is_none()
493            );
494
495            let mut parser = Parser::default();
496            assert!(
497                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("===😀"), &mut parser)
498                    .is_none()
499            );
500        }
501
502        #[test]
503        fn err_unterminated() {
504            let mut parser = Parser::default();
505
506            let maw = crate::blocks::RawDelimitedBlock::parse(
507                &BlockMetadata::new("....\nblah blah blah"),
508                &mut parser,
509            )
510            .unwrap();
511
512            assert_eq!(
513                maw.warnings,
514                vec![Warning {
515                    source: Span {
516                        data: "....",
517                        line: 1,
518                        col: 1,
519                        offset: 0,
520                    },
521                    warning: WarningType::UnterminatedDelimitedBlock,
522                }]
523            );
524        }
525    }
526
527    mod comment {
528        use crate::{
529            blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
530            tests::prelude::*,
531        };
532
533        #[test]
534        fn empty() {
535            let mut parser = Parser::default();
536            let maw = crate::blocks::RawDelimitedBlock::parse(
537                &BlockMetadata::new("////\n////"),
538                &mut parser,
539            )
540            .unwrap();
541
542            let mi = maw.item.unwrap().clone();
543
544            assert_eq!(
545                mi.item,
546                RawDelimitedBlock {
547                    content: Content {
548                        original: Span {
549                            data: "",
550                            line: 2,
551                            col: 1,
552                            offset: 5,
553                        },
554                        rendered: "",
555                    },
556                    content_model: ContentModel::Raw,
557                    context: "comment",
558                    source: Span {
559                        data: "////\n////",
560                        line: 1,
561                        col: 1,
562                        offset: 0,
563                    },
564                    title_source: None,
565                    title: None,
566                    anchor: None,
567                    anchor_reftext: None,
568                    attrlist: None,
569                    substitution_group: SubstitutionGroup::None,
570                }
571            );
572
573            assert_eq!(mi.item.content_model(), ContentModel::Raw);
574            assert_eq!(mi.item.rendered_content().unwrap(), "");
575            assert_eq!(mi.item.raw_context().as_ref(), "comment");
576            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
577            assert!(mi.item.declared_style().is_none());
578            assert!(mi.item.content().is_empty());
579            assert!(mi.item.id().is_none());
580            assert!(mi.item.roles().is_empty());
581            assert!(mi.item.options().is_empty());
582            assert!(mi.item.title_source().is_none());
583            assert!(mi.item.title().is_none());
584            assert!(mi.item.anchor().is_none());
585            assert!(mi.item.anchor_reftext().is_none());
586            assert!(mi.item.attrlist().is_none());
587            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
588        }
589
590        #[test]
591        fn multiple_lines() {
592            let mut parser = Parser::default();
593
594            let maw = crate::blocks::RawDelimitedBlock::parse(
595                &BlockMetadata::new("////\nline1  \nline2\n////"),
596                &mut parser,
597            )
598            .unwrap();
599
600            let mi = maw.item.unwrap().clone();
601
602            assert_eq!(
603                mi.item,
604                RawDelimitedBlock {
605                    content: Content {
606                        original: Span {
607                            data: "line1  \nline2",
608                            line: 2,
609                            col: 1,
610                            offset: 5,
611                        },
612                        rendered: "line1  \nline2",
613                    },
614                    content_model: ContentModel::Raw,
615                    context: "comment",
616                    source: Span {
617                        data: "////\nline1  \nline2\n////",
618                        line: 1,
619                        col: 1,
620                        offset: 0,
621                    },
622                    title_source: None,
623                    title: None,
624                    anchor: None,
625                    anchor_reftext: None,
626                    attrlist: None,
627                    substitution_group: SubstitutionGroup::None,
628                }
629            );
630
631            assert_eq!(mi.item.content_model(), ContentModel::Raw);
632            assert_eq!(mi.item.rendered_content().unwrap(), "line1  \nline2");
633            assert_eq!(mi.item.raw_context().as_ref(), "comment");
634            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
635            assert!(mi.item.declared_style().is_none());
636            assert!(mi.item.id().is_none());
637            assert!(mi.item.roles().is_empty());
638            assert!(mi.item.options().is_empty());
639            assert!(mi.item.title_source().is_none());
640            assert!(mi.item.title().is_none());
641            assert!(mi.item.anchor().is_none());
642            assert!(mi.item.anchor_reftext().is_none());
643            assert!(mi.item.attrlist().is_none());
644            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
645
646            assert_eq!(
647                mi.item.content(),
648                Content {
649                    original: Span {
650                        data: "line1  \nline2",
651                        line: 2,
652                        col: 1,
653                        offset: 5,
654                    },
655                    rendered: "line1  \nline2",
656                }
657            );
658        }
659
660        #[test]
661        fn ignores_delimiter_prefix() {
662            let mut parser = Parser::default();
663
664            let maw = crate::blocks::RawDelimitedBlock::parse(
665                &BlockMetadata::new("////\nline1  \n/////\nline2\n////"),
666                &mut parser,
667            )
668            .unwrap();
669
670            let mi = maw.item.unwrap().clone();
671
672            assert_eq!(
673                mi.item,
674                RawDelimitedBlock {
675                    content: Content {
676                        original: Span {
677                            data: "line1  \n/////\nline2",
678                            line: 2,
679                            col: 1,
680                            offset: 5,
681                        },
682                        rendered: "line1  \n/////\nline2",
683                    },
684                    content_model: ContentModel::Raw,
685                    context: "comment",
686                    source: Span {
687                        data: "////\nline1  \n/////\nline2\n////",
688                        line: 1,
689                        col: 1,
690                        offset: 0,
691                    },
692                    title_source: None,
693                    title: None,
694                    anchor: None,
695                    anchor_reftext: None,
696                    attrlist: None,
697                    substitution_group: SubstitutionGroup::None,
698                }
699            );
700
701            assert_eq!(mi.item.content_model(), ContentModel::Raw);
702            assert_eq!(mi.item.raw_context().as_ref(), "comment");
703            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
704            assert!(mi.item.declared_style().is_none());
705            assert!(mi.item.id().is_none());
706            assert!(mi.item.roles().is_empty());
707            assert!(mi.item.options().is_empty());
708            assert!(mi.item.title_source().is_none());
709            assert!(mi.item.title().is_none());
710            assert!(mi.item.anchor().is_none());
711            assert!(mi.item.anchor_reftext().is_none());
712            assert!(mi.item.attrlist().is_none());
713            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
714
715            assert_eq!(
716                mi.item.content(),
717                Content {
718                    original: Span {
719                        data: "line1  \n/////\nline2",
720                        line: 2,
721                        col: 1,
722                        offset: 5,
723                    },
724                    rendered: "line1  \n/////\nline2",
725                }
726            );
727        }
728
729        #[test]
730        fn no_panic_for_utf8_code_point_using_more_than_one_byte() {
731            let mut parser = Parser::default();
732            assert!(
733                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("///😀"), &mut parser)
734                    .is_none()
735            );
736        }
737    }
738
739    mod example {
740        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
741
742        #[test]
743        fn empty() {
744            let mut parser = Parser::default();
745            assert!(
746                crate::blocks::RawDelimitedBlock::parse(
747                    &BlockMetadata::new("====\n===="),
748                    &mut parser
749                )
750                .is_none()
751            );
752        }
753
754        #[test]
755        fn multiple_lines() {
756            let mut parser = Parser::default();
757            assert!(
758                crate::blocks::RawDelimitedBlock::parse(
759                    &BlockMetadata::new("====\nline1  \nline2\n===="),
760                    &mut parser
761                )
762                .is_none()
763            );
764        }
765    }
766
767    mod listing {
768        use crate::{
769            blocks::{ContentModel, metadata::BlockMetadata},
770            content::SubstitutionStep,
771            tests::prelude::*,
772        };
773
774        #[test]
775        fn empty() {
776            let mut parser = Parser::default();
777
778            let maw = crate::blocks::RawDelimitedBlock::parse(
779                &BlockMetadata::new("----\n----"),
780                &mut parser,
781            )
782            .unwrap();
783
784            let mi = maw.item.unwrap().clone();
785
786            assert_eq!(
787                mi.item,
788                RawDelimitedBlock {
789                    content: Content {
790                        original: Span {
791                            data: "",
792                            line: 2,
793                            col: 1,
794                            offset: 5,
795                        },
796                        rendered: "",
797                    },
798                    content_model: ContentModel::Verbatim,
799                    context: "listing",
800                    source: Span {
801                        data: "----\n----",
802                        line: 1,
803                        col: 1,
804                        offset: 0,
805                    },
806                    title_source: None,
807                    title: None,
808                    anchor: None,
809                    anchor_reftext: None,
810                    attrlist: None,
811                    substitution_group: SubstitutionGroup::Verbatim,
812                }
813            );
814
815            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
816            assert_eq!(mi.item.raw_context().as_ref(), "listing");
817            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
818            assert!(mi.item.declared_style().is_none());
819            assert!(mi.item.content().is_empty());
820            assert!(mi.item.id().is_none());
821            assert!(mi.item.roles().is_empty());
822            assert!(mi.item.options().is_empty());
823            assert!(mi.item.title_source().is_none());
824            assert!(mi.item.title().is_none());
825            assert!(mi.item.anchor().is_none());
826            assert!(mi.item.anchor_reftext().is_none());
827            assert!(mi.item.attrlist().is_none());
828            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
829        }
830
831        #[test]
832        fn multiple_lines() {
833            let mut parser = Parser::default();
834
835            let maw = crate::blocks::RawDelimitedBlock::parse(
836                &BlockMetadata::new("----\nline1  \nline2\n----"),
837                &mut parser,
838            )
839            .unwrap();
840
841            let mi = maw.item.unwrap().clone();
842
843            assert_eq!(
844                mi.item,
845                RawDelimitedBlock {
846                    content: Content {
847                        original: Span {
848                            data: "line1  \nline2",
849                            line: 2,
850                            col: 1,
851                            offset: 5,
852                        },
853                        rendered: "line1  \nline2",
854                    },
855                    content_model: ContentModel::Verbatim,
856                    context: "listing",
857                    source: Span {
858                        data: "----\nline1  \nline2\n----",
859                        line: 1,
860                        col: 1,
861                        offset: 0,
862                    },
863                    title_source: None,
864                    title: None,
865                    anchor: None,
866                    anchor_reftext: None,
867                    attrlist: None,
868                    substitution_group: SubstitutionGroup::Verbatim,
869                }
870            );
871
872            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
873            assert_eq!(mi.item.raw_context().as_ref(), "listing");
874            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
875            assert!(mi.item.declared_style().is_none());
876            assert!(mi.item.id().is_none());
877            assert!(mi.item.roles().is_empty());
878            assert!(mi.item.options().is_empty());
879            assert!(mi.item.title_source().is_none());
880            assert!(mi.item.title().is_none());
881            assert!(mi.item.anchor().is_none());
882            assert!(mi.item.anchor_reftext().is_none());
883            assert!(mi.item.attrlist().is_none());
884            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
885
886            assert_eq!(
887                mi.item.content(),
888                Content {
889                    original: Span {
890                        data: "line1  \nline2",
891                        line: 2,
892                        col: 1,
893                        offset: 5,
894                    },
895                    rendered: "line1  \nline2",
896                }
897            );
898        }
899
900        #[test]
901        fn overrides_sub_group_via_subs_attribute() {
902            let mut parser = Parser::default();
903
904            let maw = crate::blocks::RawDelimitedBlock::parse(
905                &BlockMetadata::new("[subs=quotes]\n----\nline1 < *line2*\n----"),
906                &mut parser,
907            )
908            .unwrap();
909
910            let mi = maw.item.unwrap().clone();
911
912            assert_eq!(
913                mi.item,
914                RawDelimitedBlock {
915                    content: Content {
916                        original: Span {
917                            data: "line1 < *line2*",
918                            line: 3,
919                            col: 1,
920                            offset: 19,
921                        },
922                        rendered: "line1 < <strong>line2</strong>",
923                    },
924                    content_model: ContentModel::Verbatim,
925                    context: "listing",
926                    source: Span {
927                        data: "[subs=quotes]\n----\nline1 < *line2*\n----",
928                        line: 1,
929                        col: 1,
930                        offset: 0,
931                    },
932                    title_source: None,
933                    title: None,
934                    anchor: None,
935                    anchor_reftext: None,
936                    attrlist: Some(Attrlist {
937                        attributes: &[ElementAttribute {
938                            name: Some("subs"),
939                            value: "quotes",
940                            shorthand_items: &[],
941                        },],
942                        anchor: None,
943                        source: Span {
944                            data: "subs=quotes",
945                            line: 1,
946                            col: 2,
947                            offset: 1,
948                        },
949                    },),
950                    substitution_group: SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
951                }
952            );
953
954            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
955            assert_eq!(mi.item.raw_context().as_ref(), "listing");
956            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
957            assert!(mi.item.declared_style().is_none());
958            assert!(mi.item.id().is_none());
959            assert!(mi.item.roles().is_empty());
960            assert!(mi.item.options().is_empty());
961            assert!(mi.item.title_source().is_none());
962            assert!(mi.item.title().is_none());
963            assert!(mi.item.anchor().is_none());
964            assert!(mi.item.anchor_reftext().is_none());
965
966            assert_eq!(
967                mi.item.attrlist().unwrap(),
968                Attrlist {
969                    attributes: &[ElementAttribute {
970                        name: Some("subs"),
971                        value: "quotes",
972                        shorthand_items: &[],
973                    },],
974                    anchor: None,
975                    source: Span {
976                        data: "subs=quotes",
977                        line: 1,
978                        col: 2,
979                        offset: 1,
980                    },
981                }
982            );
983
984            assert_eq!(
985                mi.item.substitution_group(),
986                SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes])
987            );
988
989            assert_eq!(
990                mi.item.content(),
991                Content {
992                    original: Span {
993                        data: "line1 < *line2*",
994                        line: 3,
995                        col: 1,
996                        offset: 19,
997                    },
998                    rendered: "line1 < <strong>line2</strong>",
999                }
1000            );
1001        }
1002
1003        #[test]
1004        fn ignores_delimiter_prefix() {
1005            let mut parser = Parser::default();
1006
1007            let maw = crate::blocks::RawDelimitedBlock::parse(
1008                &BlockMetadata::new("----\nline1  \n-----\nline2\n----"),
1009                &mut parser,
1010            )
1011            .unwrap();
1012
1013            let mi = maw.item.unwrap().clone();
1014
1015            assert_eq!(
1016                mi.item,
1017                RawDelimitedBlock {
1018                    content: Content {
1019                        original: Span {
1020                            data: "line1  \n-----\nline2",
1021                            line: 2,
1022                            col: 1,
1023                            offset: 5,
1024                        },
1025                        rendered: "line1  \n-----\nline2",
1026                    },
1027                    content_model: ContentModel::Verbatim,
1028                    context: "listing",
1029                    source: Span {
1030                        data: "----\nline1  \n-----\nline2\n----",
1031                        line: 1,
1032                        col: 1,
1033                        offset: 0,
1034                    },
1035                    title_source: None,
1036                    title: None,
1037                    anchor: None,
1038                    anchor_reftext: None,
1039                    attrlist: None,
1040                    substitution_group: SubstitutionGroup::Verbatim,
1041                }
1042            );
1043
1044            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
1045            assert_eq!(mi.item.raw_context().as_ref(), "listing");
1046            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
1047            assert!(mi.item.declared_style().is_none());
1048            assert!(mi.item.id().is_none());
1049            assert!(mi.item.roles().is_empty());
1050            assert!(mi.item.options().is_empty());
1051            assert!(mi.item.title_source().is_none());
1052            assert!(mi.item.title().is_none());
1053            assert!(mi.item.anchor().is_none());
1054            assert!(mi.item.anchor_reftext().is_none());
1055            assert!(mi.item.attrlist().is_none());
1056            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
1057
1058            assert_eq!(
1059                mi.item.content(),
1060                Content {
1061                    original: Span {
1062                        data: "line1  \n-----\nline2",
1063                        line: 2,
1064                        col: 1,
1065                        offset: 5,
1066                    },
1067                    rendered: "line1  \n-----\nline2",
1068                }
1069            );
1070
1071            assert_eq!(
1072                mi.item.content(),
1073                Content {
1074                    original: Span {
1075                        data: "line1  \n-----\nline2",
1076                        line: 2,
1077                        col: 1,
1078                        offset: 5,
1079                    },
1080                    rendered: "line1  \n-----\nline2",
1081                }
1082            );
1083        }
1084    }
1085
1086    mod sidebar {
1087        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1088
1089        #[test]
1090        fn empty() {
1091            let mut parser = Parser::default();
1092            assert!(
1093                crate::blocks::RawDelimitedBlock::parse(
1094                    &BlockMetadata::new("****\n****"),
1095                    &mut parser
1096                )
1097                .is_none()
1098            );
1099        }
1100
1101        #[test]
1102        fn multiple_lines() {
1103            let mut parser = Parser::default();
1104            assert!(
1105                crate::blocks::RawDelimitedBlock::parse(
1106                    &BlockMetadata::new("****\nline1  \nline2\n****"),
1107                    &mut parser
1108                )
1109                .is_none()
1110            );
1111        }
1112    }
1113
1114    mod table {
1115        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1116
1117        #[test]
1118        fn empty() {
1119            let mut parser = Parser::default();
1120            assert!(
1121                crate::blocks::RawDelimitedBlock::parse(
1122                    &BlockMetadata::new("|===\n|==="),
1123                    &mut parser
1124                )
1125                .is_none()
1126            );
1127
1128            let mut parser = Parser::default();
1129            assert!(
1130                crate::blocks::RawDelimitedBlock::parse(
1131                    &BlockMetadata::new(",===\n,==="),
1132                    &mut parser
1133                )
1134                .is_none()
1135            );
1136
1137            let mut parser = Parser::default();
1138            assert!(
1139                crate::blocks::RawDelimitedBlock::parse(
1140                    &BlockMetadata::new(":===\n:==="),
1141                    &mut parser
1142                )
1143                .is_none()
1144            );
1145
1146            let mut parser = Parser::default();
1147            assert!(
1148                crate::blocks::RawDelimitedBlock::parse(
1149                    &BlockMetadata::new("!===\n!==="),
1150                    &mut parser
1151                )
1152                .is_none()
1153            );
1154        }
1155
1156        #[test]
1157        fn multiple_lines() {
1158            let mut parser = Parser::default();
1159            assert!(
1160                crate::blocks::RawDelimitedBlock::parse(
1161                    &BlockMetadata::new("|===\nline1  \nline2\n|==="),
1162                    &mut parser
1163                )
1164                .is_none()
1165            );
1166
1167            let mut parser = Parser::default();
1168            assert!(
1169                crate::blocks::RawDelimitedBlock::parse(
1170                    &BlockMetadata::new(",===\nline1  \nline2\n,==="),
1171                    &mut parser
1172                )
1173                .is_none()
1174            );
1175
1176            let mut parser = Parser::default();
1177            assert!(
1178                crate::blocks::RawDelimitedBlock::parse(
1179                    &BlockMetadata::new(":===\nline1  \nline2\n:==="),
1180                    &mut parser
1181                )
1182                .is_none()
1183            );
1184
1185            let mut parser = Parser::default();
1186            assert!(
1187                crate::blocks::RawDelimitedBlock::parse(
1188                    &BlockMetadata::new("!===\nline1  \nline2\n!==="),
1189                    &mut parser
1190                )
1191                .is_none()
1192            );
1193        }
1194    }
1195
1196    mod pass {
1197        use crate::{
1198            blocks::{ContentModel, metadata::BlockMetadata},
1199            tests::prelude::*,
1200        };
1201
1202        #[test]
1203        fn empty() {
1204            let mut parser = Parser::default();
1205            let maw = crate::blocks::RawDelimitedBlock::parse(
1206                &BlockMetadata::new("++++\n++++"),
1207                &mut parser,
1208            )
1209            .unwrap();
1210
1211            let mi = maw.item.unwrap().clone();
1212
1213            assert_eq!(
1214                mi.item,
1215                RawDelimitedBlock {
1216                    content: Content {
1217                        original: Span {
1218                            data: "",
1219                            line: 2,
1220                            col: 1,
1221                            offset: 5,
1222                        },
1223                        rendered: "",
1224                    },
1225                    content_model: ContentModel::Raw,
1226                    context: "pass",
1227                    source: Span {
1228                        data: "++++\n++++",
1229                        line: 1,
1230                        col: 1,
1231                        offset: 0,
1232                    },
1233                    title_source: None,
1234                    title: None,
1235                    anchor: None,
1236                    anchor_reftext: None,
1237                    attrlist: None,
1238                    substitution_group: SubstitutionGroup::Pass,
1239                }
1240            );
1241
1242            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1243            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1244            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1245            assert!(mi.item.declared_style().is_none());
1246            assert!(mi.item.content().is_empty());
1247            assert!(mi.item.id().is_none());
1248            assert!(mi.item.roles().is_empty());
1249            assert!(mi.item.options().is_empty());
1250            assert!(mi.item.title_source().is_none());
1251            assert!(mi.item.title().is_none());
1252            assert!(mi.item.anchor().is_none());
1253            assert!(mi.item.anchor_reftext().is_none());
1254            assert!(mi.item.attrlist().is_none());
1255            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1256        }
1257
1258        #[test]
1259        fn multiple_lines() {
1260            let mut parser = Parser::default();
1261
1262            let maw = crate::blocks::RawDelimitedBlock::parse(
1263                &BlockMetadata::new("++++\nline1  \nline2\n++++"),
1264                &mut parser,
1265            )
1266            .unwrap();
1267
1268            let mi = maw.item.unwrap().clone();
1269
1270            assert_eq!(
1271                mi.item,
1272                RawDelimitedBlock {
1273                    content: Content {
1274                        original: Span {
1275                            data: "line1  \nline2",
1276                            line: 2,
1277                            col: 1,
1278                            offset: 5,
1279                        },
1280                        rendered: "line1  \nline2",
1281                    },
1282                    content_model: ContentModel::Raw,
1283                    context: "pass",
1284                    source: Span {
1285                        data: "++++\nline1  \nline2\n++++",
1286                        line: 1,
1287                        col: 1,
1288                        offset: 0,
1289                    },
1290                    title_source: None,
1291                    title: None,
1292                    anchor: None,
1293                    anchor_reftext: None,
1294                    attrlist: None,
1295                    substitution_group: SubstitutionGroup::Pass,
1296                }
1297            );
1298
1299            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1300            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1301            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1302            assert!(mi.item.declared_style().is_none());
1303            assert!(mi.item.id().is_none());
1304            assert!(mi.item.roles().is_empty());
1305            assert!(mi.item.options().is_empty());
1306            assert!(mi.item.title_source().is_none());
1307            assert!(mi.item.title().is_none());
1308            assert!(mi.item.anchor().is_none());
1309            assert!(mi.item.anchor_reftext().is_none());
1310            assert!(mi.item.attrlist().is_none());
1311            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1312
1313            assert_eq!(
1314                mi.item.content(),
1315                Content {
1316                    original: Span {
1317                        data: "line1  \nline2",
1318                        line: 2,
1319                        col: 1,
1320                        offset: 5,
1321                    },
1322                    rendered: "line1  \nline2",
1323                }
1324            );
1325        }
1326
1327        #[test]
1328        fn ignores_delimiter_prefix() {
1329            let mut parser = Parser::default();
1330
1331            let maw = crate::blocks::RawDelimitedBlock::parse(
1332                &BlockMetadata::new("++++\nline1  \n+++++\nline2\n++++"),
1333                &mut parser,
1334            )
1335            .unwrap();
1336
1337            let mi = maw.item.unwrap().clone();
1338
1339            assert_eq!(
1340                mi.item,
1341                RawDelimitedBlock {
1342                    content: Content {
1343                        original: Span {
1344                            data: "line1  \n+++++\nline2",
1345                            line: 2,
1346                            col: 1,
1347                            offset: 5,
1348                        },
1349                        rendered: "line1  \n+++++\nline2",
1350                    },
1351                    content_model: ContentModel::Raw,
1352                    context: "pass",
1353                    source: Span {
1354                        data: "++++\nline1  \n+++++\nline2\n++++",
1355                        line: 1,
1356                        col: 1,
1357                        offset: 0,
1358                    },
1359                    title_source: None,
1360                    title: None,
1361                    anchor: None,
1362                    anchor_reftext: None,
1363                    attrlist: None,
1364                    substitution_group: SubstitutionGroup::Pass,
1365                }
1366            );
1367
1368            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1369            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1370            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1371            assert!(mi.item.declared_style().is_none());
1372            assert!(mi.item.id().is_none());
1373            assert!(mi.item.roles().is_empty());
1374            assert!(mi.item.options().is_empty());
1375            assert!(mi.item.title_source().is_none());
1376            assert!(mi.item.title().is_none());
1377            assert!(mi.item.anchor().is_none());
1378            assert!(mi.item.anchor_reftext().is_none());
1379            assert!(mi.item.attrlist().is_none());
1380            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1381
1382            assert_eq!(
1383                mi.item.content(),
1384                Content {
1385                    original: Span {
1386                        data: "line1  \n+++++\nline2",
1387                        line: 2,
1388                        col: 1,
1389                        offset: 5,
1390                    },
1391                    rendered: "line1  \n+++++\nline2",
1392                }
1393            );
1394        }
1395    }
1396
1397    mod stem {
1398        use crate::{blocks::ContentModel, tests::prelude::*};
1399
1400        /// A `[stem]` passthrough block becomes a `stem` block whose expression
1401        /// has only the special characters substitution applied. The notation's
1402        /// math delimiters are added by the converter at render time, so they
1403        /// do not appear in the parsed content.
1404        #[test]
1405        fn stem_style_block() {
1406            let doc = Parser::default().parse("[stem]\n++++\na < b\n++++");
1407            let block = doc.nested_blocks().next().unwrap();
1408
1409            assert_eq!(block.content_model(), ContentModel::Raw);
1410            assert_eq!(block.raw_context().as_ref(), "stem");
1411            assert_eq!(block.resolved_context().as_ref(), "stem");
1412            assert_eq!(block.declared_style(), Some("stem"));
1413            assert_eq!(block.rendered_content(), Some("a &lt; b"));
1414            assert_eq!(block.substitution_group(), SubstitutionGroup::Stem);
1415            assert!(doc.warnings().next().is_none());
1416        }
1417
1418        #[test]
1419        fn asciimath_style_block() {
1420            let doc = Parser::default().parse("[asciimath]\n++++\nx^2\n++++");
1421            let block = doc.nested_blocks().next().unwrap();
1422
1423            assert_eq!(block.raw_context().as_ref(), "stem");
1424            assert_eq!(block.declared_style(), Some("asciimath"));
1425            assert_eq!(block.rendered_content(), Some("x^2"));
1426        }
1427
1428        #[test]
1429        fn latexmath_style_block() {
1430            let doc = Parser::default().parse("[latexmath]\n++++\nC = \\alpha\n++++");
1431            let block = doc.nested_blocks().next().unwrap();
1432
1433            assert_eq!(block.raw_context().as_ref(), "stem");
1434            assert_eq!(block.declared_style(), Some("latexmath"));
1435            assert_eq!(block.rendered_content(), Some(r"C = \alpha"));
1436        }
1437
1438        /// Without a STEM style, a `++++` block remains a `pass` block with no
1439        /// substitutions applied.
1440        #[test]
1441        fn unstyled_block_is_still_pass() {
1442            let doc = Parser::default().parse("++++\na < b\n++++");
1443            let block = doc.nested_blocks().next().unwrap();
1444
1445            assert_eq!(block.raw_context().as_ref(), "pass");
1446            assert_eq!(block.rendered_content(), Some("a < b"));
1447            assert_eq!(block.substitution_group(), SubstitutionGroup::Pass);
1448        }
1449
1450        /// An explicit `subs` attribute still overrides a STEM block's default
1451        /// substitution group.
1452        #[test]
1453        fn subs_attribute_overrides_stem_default() {
1454            let doc = Parser::default().parse("[stem,subs=none]\n++++\na < b\n++++");
1455            let block = doc.nested_blocks().next().unwrap();
1456
1457            assert_eq!(block.raw_context().as_ref(), "stem");
1458            assert_eq!(block.rendered_content(), Some("a < b"));
1459            assert_eq!(block.substitution_group(), SubstitutionGroup::None);
1460        }
1461    }
1462
1463    mod quote {
1464        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1465
1466        #[test]
1467        fn empty() {
1468            let mut parser = Parser::default();
1469            assert!(
1470                crate::blocks::RawDelimitedBlock::parse(
1471                    &BlockMetadata::new("____\n____"),
1472                    &mut parser
1473                )
1474                .is_none()
1475            );
1476        }
1477
1478        #[test]
1479        fn multiple_lines() {
1480            let mut parser = Parser::default();
1481            assert!(
1482                crate::blocks::RawDelimitedBlock::parse(
1483                    &BlockMetadata::new("____\nline1  \nline2\n____"),
1484                    &mut parser
1485                )
1486                .is_none()
1487            );
1488        }
1489    }
1490}