1use crate::{
2 HasSpan, Parser, Span,
3 attributes::Attrlist,
4 blocks::{ContentModel, IsBlock, caption::assign_block_caption, metadata::BlockMetadata},
5 content::{Content, SubstitutionGroup},
6 span::MatchedItem,
7 strings::CowStr,
8 warnings::{MatchAndWarnings, Warning, WarningType},
9};
10
11#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct RawDelimitedBlock<'src> {
35 content: Content<'src>,
36 content_model: ContentModel,
37 context: CowStr<'src>,
38 source: Span<'src>,
39 title_source: Option<Span<'src>>,
40 title: Option<String>,
41 caption: Option<String>,
42 number: Option<usize>,
43 anchor: Option<Span<'src>>,
44 anchor_reftext: Option<Span<'src>>,
45 attrlist: Option<Attrlist<'src>>,
46 substitution_group: SubstitutionGroup,
47}
48
49impl<'src> RawDelimitedBlock<'src> {
50 pub(crate) fn is_valid_delimiter(line: &Span<'src>) -> bool {
51 let data = line.data();
52
53 if data == "```" {
58 return true;
59 }
60
61 if data.len() >= 4 {
66 if data.starts_with("////") {
67 data.split_at(4).1.chars().all(|c| c == '/')
68 } else if data.starts_with("----") {
69 data.split_at(4).1.chars().all(|c| c == '-')
70 } else if data.starts_with("....") {
71 data.split_at(4).1.chars().all(|c| c == '.')
72 } else if data.starts_with("++++") {
73 data.split_at(4).1.chars().all(|c| c == '+')
74 } else {
75 false
76 }
77 } else {
78 false
79 }
80 }
81
82 pub(crate) fn parse(
83 metadata: &BlockMetadata<'src>,
84 parser: &mut Parser,
85 ) -> Option<MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>>> {
86 let delimiter = metadata.block_start.take_normalized_line();
87 let delimiter_data = delimiter.item.data();
88
89 let (content_model, context, mut substitution_group) = if delimiter_data == "--" {
94 open_block_verbatim_masquerade(metadata.attrlist.as_ref())?
97 } else if delimiter_data == "```" {
98 (
103 ContentModel::Verbatim,
104 "listing",
105 SubstitutionGroup::Verbatim,
106 )
107 } else {
108 if delimiter.item.len() < 4 {
109 return None;
110 }
111
112 let block_type = match delimiter_data
113 .split_at_checked(delimiter_data.len().min(4))?
114 .0
115 {
116 "////" => (ContentModel::Raw, "comment", SubstitutionGroup::None),
117 "----" => (
118 ContentModel::Verbatim,
119 "listing",
120 SubstitutionGroup::Verbatim,
121 ),
122 "...." => (
123 ContentModel::Verbatim,
124 "literal",
125 SubstitutionGroup::Verbatim,
126 ),
127 "++++" => pass_or_stem_block_type(metadata.attrlist.as_ref()),
128 _ => {
129 return None;
130 }
131 };
132
133 if !Self::is_valid_delimiter(&delimiter.item) {
137 return None;
138 }
139
140 block_type
141 };
142
143 let caption = assign_block_caption(
150 parser,
151 context,
152 metadata.attrlist.as_ref(),
153 metadata.title.is_some(),
154 );
155 let number = caption.as_ref().and_then(|c| c.number);
156 let caption = caption.map(|c| c.prefix);
157
158 let content_start = delimiter.after;
159 let mut next = content_start;
160
161 while !next.is_empty() {
162 let line = next.take_normalized_line();
163 if line.item.data() == delimiter.item.data() {
164 let content = content_start.trim_remainder(next).trim_trailing_line_end();
165
166 let mut content: Content<'src> = content.into();
167
168 if context != "comment" {
173 substitution_group =
174 substitution_group.override_via_attrlist(metadata.attrlist.as_ref());
175 }
176
177 substitution_group.apply(&mut content, parser, metadata.attrlist.as_ref());
178
179 return Some(MatchAndWarnings {
180 item: Some(MatchedItem {
181 item: Self {
182 content,
183 content_model,
184 context: context.into(),
185 source: metadata
186 .source
187 .trim_remainder(line.after)
188 .trim_trailing_line_end(),
189 title_source: metadata.title_source,
190 title: metadata.title.clone(),
191 caption: caption.clone(),
192 number,
193 anchor: metadata.anchor,
194 anchor_reftext: metadata.anchor_reftext,
195 attrlist: metadata.attrlist.clone(),
196 substitution_group,
197 },
198 after: line.after,
199 }),
200 warnings: vec![],
201 });
202 }
203
204 next = line.after;
205 }
206
207 let content = content_start.trim_remainder(next).trim_trailing_line_end();
208
209 Some(MatchAndWarnings {
210 item: Some(MatchedItem {
211 item: Self {
212 content: content.into(),
213 content_model,
214 context: context.into(),
215 source: metadata
216 .source
217 .trim_remainder(next)
218 .trim_trailing_line_end(),
219 title_source: metadata.title_source,
220 title: metadata.title.clone(),
221 caption,
222 number,
223 anchor: metadata.anchor,
224 anchor_reftext: metadata.anchor_reftext,
225 attrlist: metadata.attrlist.clone(),
226 substitution_group,
227 },
228 after: next,
229 }),
230 warnings: vec![Warning {
231 source: delimiter.item,
232 warning: WarningType::UnterminatedDelimitedBlock,
233 }],
234 })
235 }
236
237 pub fn content(&self) -> &Content<'src> {
239 &self.content
240 }
241}
242
243fn open_block_verbatim_masquerade(
264 attrlist: Option<&Attrlist<'_>>,
265) -> Option<(ContentModel, &'static str, SubstitutionGroup)> {
266 match attrlist?.block_style()? {
267 "source" | "listing" => Some((
268 ContentModel::Verbatim,
269 "listing",
270 SubstitutionGroup::Verbatim,
271 )),
272 "literal" => Some((
273 ContentModel::Verbatim,
274 "literal",
275 SubstitutionGroup::Verbatim,
276 )),
277 "pass" => Some((ContentModel::Raw, "pass", SubstitutionGroup::Pass)),
278 "comment" => Some((ContentModel::Raw, "comment", SubstitutionGroup::None)),
279 _ => None,
280 }
281}
282
283fn pass_or_stem_block_type(
292 attrlist: Option<&Attrlist<'_>>,
293) -> (ContentModel, &'static str, SubstitutionGroup) {
294 match attrlist.and_then(|a| a.block_style()) {
295 Some("stem") | Some("asciimath") | Some("latexmath") => {
296 (ContentModel::Raw, "stem", SubstitutionGroup::Stem)
297 }
298 _ => (ContentModel::Raw, "pass", SubstitutionGroup::Pass),
299 }
300}
301
302impl<'src> IsBlock<'src> for RawDelimitedBlock<'src> {
303 fn content_model(&self) -> ContentModel {
304 self.content_model
305 }
306
307 fn content_mut(&mut self) -> Option<&mut Content<'src>> {
308 Some(&mut self.content)
309 }
310
311 fn rendered_content(&self) -> Option<&str> {
312 Some(self.content.rendered())
313 }
314
315 fn raw_context(&self) -> CowStr<'src> {
316 self.context.clone()
317 }
318
319 fn title_source(&'src self) -> Option<Span<'src>> {
320 self.title_source
321 }
322
323 fn title(&self) -> Option<&str> {
324 self.title.as_deref()
325 }
326
327 fn caption(&self) -> Option<&str> {
328 self.caption.as_deref()
329 }
330
331 fn number(&self) -> Option<usize> {
332 self.number
333 }
334
335 fn anchor(&'src self) -> Option<Span<'src>> {
336 self.anchor
337 }
338
339 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
340 self.anchor_reftext
341 }
342
343 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
344 self.attrlist.as_ref()
345 }
346
347 fn substitution_group(&'src self) -> SubstitutionGroup {
348 self.substitution_group.clone()
349 }
350}
351
352impl<'src> HasSpan<'src> for RawDelimitedBlock<'src> {
353 fn span(&self) -> Span<'src> {
354 self.source
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 #![allow(clippy::unwrap_used)]
361
362 mod is_valid_delimiter {
363 use crate::blocks::RawDelimitedBlock;
364
365 #[test]
366 fn comment() {
367 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
368 "////"
369 )));
370 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
371 "/////"
372 )));
373 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
374 "/////////"
375 )));
376
377 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
378 "///"
379 )));
380 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
381 "//-/"
382 )));
383 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
384 "////-"
385 )));
386 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
387 "//////////x"
388 )));
389 }
390
391 #[test]
392 fn example() {
393 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
394 "===="
395 )));
396 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
397 "====="
398 )));
399 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
400 "==="
401 )));
402 }
403
404 #[test]
405 fn listing() {
406 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
407 "----"
408 )));
409 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
410 "-----"
411 )));
412 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
413 "---------"
414 )));
415
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 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
426 "----------x"
427 )));
428 }
429
430 #[test]
431 fn fenced() {
432 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
434 "```"
435 )));
436
437 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
440 "````"
441 )));
442 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
443 "``"
444 )));
445 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
446 "```java"
447 )));
448 }
449
450 #[test]
451 fn literal() {
452 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
453 "...."
454 )));
455 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
456 "....."
457 )));
458 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
459 "........."
460 )));
461
462 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
463 "..."
464 )));
465 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
466 "../."
467 )));
468 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
469 "..../"
470 )));
471 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
472 "..........x"
473 )));
474 }
475
476 #[test]
477 fn sidebar() {
478 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
479 "****"
480 )));
481 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
482 "*****"
483 )));
484 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
485 "***"
486 )));
487 }
488
489 #[test]
490 fn table() {
491 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
492 "|==="
493 )));
494 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
495 ",==="
496 )));
497 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
498 ":==="
499 )));
500 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
501 "!==="
502 )));
503 }
504
505 #[test]
506 fn pass() {
507 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
508 "++++"
509 )));
510 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
511 "+++++"
512 )));
513 assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
514 "+++++++++"
515 )));
516
517 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
518 "+++"
519 )));
520 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
521 "++/+"
522 )));
523 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
524 "++++/"
525 )));
526 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
527 "++++++++++x"
528 )));
529 }
530
531 #[test]
532 fn quote() {
533 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
534 "____"
535 )));
536 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
537 "_____"
538 )));
539 assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
540 "___"
541 )));
542 }
543 }
544
545 mod parse {
546 use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
547
548 #[test]
549 fn err_invalid_delimiter() {
550 let mut parser = Parser::default();
551 assert!(
552 crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new(""), &mut parser)
553 .is_none()
554 );
555
556 let mut parser = Parser::default();
557 assert!(
558 crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("..."), &mut parser)
559 .is_none()
560 );
561
562 let mut parser = Parser::default();
563 assert!(
564 crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("++++x"), &mut parser)
565 .is_none()
566 );
567
568 let mut parser = Parser::default();
569 assert!(
570 crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("____x"), &mut parser)
571 .is_none()
572 );
573
574 let mut parser = Parser::default();
575 assert!(
576 crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("====x"), &mut parser)
577 .is_none()
578 );
579
580 let mut parser = Parser::default();
581 assert!(
582 crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("==\n=="), &mut parser)
583 .is_none()
584 );
585
586 let mut parser = Parser::default();
587 assert!(
588 crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("===😀"), &mut parser)
589 .is_none()
590 );
591 }
592
593 #[test]
594 fn err_unterminated() {
595 let mut parser = Parser::default();
596
597 let maw = crate::blocks::RawDelimitedBlock::parse(
598 &BlockMetadata::new("....\nblah blah blah"),
599 &mut parser,
600 )
601 .unwrap();
602
603 assert_eq!(
604 maw.warnings,
605 vec![Warning {
606 source: Span {
607 data: "....",
608 line: 1,
609 col: 1,
610 offset: 0,
611 },
612 warning: WarningType::UnterminatedDelimitedBlock,
613 }]
614 );
615 }
616 }
617
618 mod comment {
619 use crate::{
620 blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
621 tests::prelude::*,
622 };
623
624 #[test]
625 fn empty() {
626 let mut parser = Parser::default();
627 let maw = crate::blocks::RawDelimitedBlock::parse(
628 &BlockMetadata::new("////\n////"),
629 &mut parser,
630 )
631 .unwrap();
632
633 let mi = maw.item.unwrap().clone();
634
635 assert_eq!(
636 mi.item,
637 RawDelimitedBlock {
638 content: Content {
639 original: Span {
640 data: "",
641 line: 2,
642 col: 1,
643 offset: 5,
644 },
645 rendered: "",
646 },
647 content_model: ContentModel::Raw,
648 context: "comment",
649 source: Span {
650 data: "////\n////",
651 line: 1,
652 col: 1,
653 offset: 0,
654 },
655 title_source: None,
656 title: None,
657 caption: None,
658 number: None,
659 anchor: None,
660 anchor_reftext: None,
661 attrlist: None,
662 substitution_group: SubstitutionGroup::None,
663 }
664 );
665
666 assert_eq!(mi.item.content_model(), ContentModel::Raw);
667 assert_eq!(mi.item.rendered_content().unwrap(), "");
668 assert_eq!(mi.item.raw_context().as_ref(), "comment");
669 assert_eq!(mi.item.resolved_context().as_ref(), "comment");
670 assert!(mi.item.declared_style().is_none());
671 assert!(mi.item.content().is_empty());
672 assert!(mi.item.id().is_none());
673 assert!(mi.item.roles().is_empty());
674 assert!(mi.item.options().is_empty());
675 assert!(mi.item.title_source().is_none());
676 assert!(mi.item.title().is_none());
677 assert!(mi.item.anchor().is_none());
678 assert!(mi.item.anchor_reftext().is_none());
679 assert!(mi.item.attrlist().is_none());
680 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
681 }
682
683 #[test]
684 fn multiple_lines() {
685 let mut parser = Parser::default();
686
687 let maw = crate::blocks::RawDelimitedBlock::parse(
688 &BlockMetadata::new("////\nline1 \nline2\n////"),
689 &mut parser,
690 )
691 .unwrap();
692
693 let mi = maw.item.unwrap().clone();
694
695 assert_eq!(
696 mi.item,
697 RawDelimitedBlock {
698 content: Content {
699 original: Span {
700 data: "line1 \nline2",
701 line: 2,
702 col: 1,
703 offset: 5,
704 },
705 rendered: "line1 \nline2",
706 },
707 content_model: ContentModel::Raw,
708 context: "comment",
709 source: Span {
710 data: "////\nline1 \nline2\n////",
711 line: 1,
712 col: 1,
713 offset: 0,
714 },
715 title_source: None,
716 title: None,
717 caption: None,
718 number: None,
719 anchor: None,
720 anchor_reftext: None,
721 attrlist: None,
722 substitution_group: SubstitutionGroup::None,
723 }
724 );
725
726 assert_eq!(mi.item.content_model(), ContentModel::Raw);
727 assert_eq!(mi.item.rendered_content().unwrap(), "line1 \nline2");
728 assert_eq!(mi.item.raw_context().as_ref(), "comment");
729 assert_eq!(mi.item.resolved_context().as_ref(), "comment");
730 assert!(mi.item.declared_style().is_none());
731 assert!(mi.item.id().is_none());
732 assert!(mi.item.roles().is_empty());
733 assert!(mi.item.options().is_empty());
734 assert!(mi.item.title_source().is_none());
735 assert!(mi.item.title().is_none());
736 assert!(mi.item.anchor().is_none());
737 assert!(mi.item.anchor_reftext().is_none());
738 assert!(mi.item.attrlist().is_none());
739 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
740
741 assert_eq!(
742 mi.item.content(),
743 Content {
744 original: Span {
745 data: "line1 \nline2",
746 line: 2,
747 col: 1,
748 offset: 5,
749 },
750 rendered: "line1 \nline2",
751 }
752 );
753 }
754
755 #[test]
756 fn ignores_delimiter_prefix() {
757 let mut parser = Parser::default();
758
759 let maw = crate::blocks::RawDelimitedBlock::parse(
760 &BlockMetadata::new("////\nline1 \n/////\nline2\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: "line1 \n/////\nline2",
773 line: 2,
774 col: 1,
775 offset: 5,
776 },
777 rendered: "line1 \n/////\nline2",
778 },
779 content_model: ContentModel::Raw,
780 context: "comment",
781 source: Span {
782 data: "////\nline1 \n/////\nline2\n////",
783 line: 1,
784 col: 1,
785 offset: 0,
786 },
787 title_source: None,
788 title: None,
789 caption: None,
790 number: None,
791 anchor: None,
792 anchor_reftext: None,
793 attrlist: None,
794 substitution_group: SubstitutionGroup::None,
795 }
796 );
797
798 assert_eq!(mi.item.content_model(), ContentModel::Raw);
799 assert_eq!(mi.item.raw_context().as_ref(), "comment");
800 assert_eq!(mi.item.resolved_context().as_ref(), "comment");
801 assert!(mi.item.declared_style().is_none());
802 assert!(mi.item.id().is_none());
803 assert!(mi.item.roles().is_empty());
804 assert!(mi.item.options().is_empty());
805 assert!(mi.item.title_source().is_none());
806 assert!(mi.item.title().is_none());
807 assert!(mi.item.anchor().is_none());
808 assert!(mi.item.anchor_reftext().is_none());
809 assert!(mi.item.attrlist().is_none());
810 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
811
812 assert_eq!(
813 mi.item.content(),
814 Content {
815 original: Span {
816 data: "line1 \n/////\nline2",
817 line: 2,
818 col: 1,
819 offset: 5,
820 },
821 rendered: "line1 \n/////\nline2",
822 }
823 );
824 }
825
826 #[test]
827 fn no_panic_for_utf8_code_point_using_more_than_one_byte() {
828 let mut parser = Parser::default();
829 assert!(
830 crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("///😀"), &mut parser)
831 .is_none()
832 );
833 }
834 }
835
836 mod example {
837 use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
838
839 #[test]
840 fn empty() {
841 let mut parser = Parser::default();
842 assert!(
843 crate::blocks::RawDelimitedBlock::parse(
844 &BlockMetadata::new("====\n===="),
845 &mut parser
846 )
847 .is_none()
848 );
849 }
850
851 #[test]
852 fn multiple_lines() {
853 let mut parser = Parser::default();
854 assert!(
855 crate::blocks::RawDelimitedBlock::parse(
856 &BlockMetadata::new("====\nline1 \nline2\n===="),
857 &mut parser
858 )
859 .is_none()
860 );
861 }
862 }
863
864 mod listing {
865 use crate::{
866 blocks::{ContentModel, metadata::BlockMetadata},
867 content::SubstitutionStep,
868 tests::prelude::*,
869 };
870
871 #[test]
872 fn empty() {
873 let mut parser = Parser::default();
874
875 let maw = crate::blocks::RawDelimitedBlock::parse(
876 &BlockMetadata::new("----\n----"),
877 &mut parser,
878 )
879 .unwrap();
880
881 let mi = maw.item.unwrap().clone();
882
883 assert_eq!(
884 mi.item,
885 RawDelimitedBlock {
886 content: Content {
887 original: Span {
888 data: "",
889 line: 2,
890 col: 1,
891 offset: 5,
892 },
893 rendered: "",
894 },
895 content_model: ContentModel::Verbatim,
896 context: "listing",
897 source: Span {
898 data: "----\n----",
899 line: 1,
900 col: 1,
901 offset: 0,
902 },
903 title_source: None,
904 title: None,
905 caption: None,
906 number: None,
907 anchor: None,
908 anchor_reftext: None,
909 attrlist: None,
910 substitution_group: SubstitutionGroup::Verbatim,
911 }
912 );
913
914 assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
915 assert_eq!(mi.item.raw_context().as_ref(), "listing");
916 assert_eq!(mi.item.resolved_context().as_ref(), "listing");
917 assert!(mi.item.declared_style().is_none());
918 assert!(mi.item.content().is_empty());
919 assert!(mi.item.id().is_none());
920 assert!(mi.item.roles().is_empty());
921 assert!(mi.item.options().is_empty());
922 assert!(mi.item.title_source().is_none());
923 assert!(mi.item.title().is_none());
924 assert!(mi.item.anchor().is_none());
925 assert!(mi.item.anchor_reftext().is_none());
926 assert!(mi.item.attrlist().is_none());
927 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
928 }
929
930 #[test]
931 fn multiple_lines() {
932 let mut parser = Parser::default();
933
934 let maw = crate::blocks::RawDelimitedBlock::parse(
935 &BlockMetadata::new("----\nline1 \nline2\n----"),
936 &mut parser,
937 )
938 .unwrap();
939
940 let mi = maw.item.unwrap().clone();
941
942 assert_eq!(
943 mi.item,
944 RawDelimitedBlock {
945 content: Content {
946 original: Span {
947 data: "line1 \nline2",
948 line: 2,
949 col: 1,
950 offset: 5,
951 },
952 rendered: "line1 \nline2",
953 },
954 content_model: ContentModel::Verbatim,
955 context: "listing",
956 source: Span {
957 data: "----\nline1 \nline2\n----",
958 line: 1,
959 col: 1,
960 offset: 0,
961 },
962 title_source: None,
963 title: None,
964 caption: None,
965 number: None,
966 anchor: None,
967 anchor_reftext: None,
968 attrlist: None,
969 substitution_group: SubstitutionGroup::Verbatim,
970 }
971 );
972
973 assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
974 assert_eq!(mi.item.raw_context().as_ref(), "listing");
975 assert_eq!(mi.item.resolved_context().as_ref(), "listing");
976 assert!(mi.item.declared_style().is_none());
977 assert!(mi.item.id().is_none());
978 assert!(mi.item.roles().is_empty());
979 assert!(mi.item.options().is_empty());
980 assert!(mi.item.title_source().is_none());
981 assert!(mi.item.title().is_none());
982 assert!(mi.item.anchor().is_none());
983 assert!(mi.item.anchor_reftext().is_none());
984 assert!(mi.item.attrlist().is_none());
985 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
986
987 assert_eq!(
988 mi.item.content(),
989 Content {
990 original: Span {
991 data: "line1 \nline2",
992 line: 2,
993 col: 1,
994 offset: 5,
995 },
996 rendered: "line1 \nline2",
997 }
998 );
999 }
1000
1001 #[test]
1002 fn overrides_sub_group_via_subs_attribute() {
1003 let mut parser = Parser::default();
1004
1005 let maw = crate::blocks::RawDelimitedBlock::parse(
1006 &BlockMetadata::new("[subs=quotes]\n----\nline1 < *line2*\n----"),
1007 &mut parser,
1008 )
1009 .unwrap();
1010
1011 let mi = maw.item.unwrap().clone();
1012
1013 assert_eq!(
1014 mi.item,
1015 RawDelimitedBlock {
1016 content: Content {
1017 original: Span {
1018 data: "line1 < *line2*",
1019 line: 3,
1020 col: 1,
1021 offset: 19,
1022 },
1023 rendered: "line1 < <strong>line2</strong>",
1024 },
1025 content_model: ContentModel::Verbatim,
1026 context: "listing",
1027 source: Span {
1028 data: "[subs=quotes]\n----\nline1 < *line2*\n----",
1029 line: 1,
1030 col: 1,
1031 offset: 0,
1032 },
1033 title_source: None,
1034 title: None,
1035 caption: None,
1036 number: None,
1037 anchor: None,
1038 anchor_reftext: None,
1039 attrlist: Some(Attrlist {
1040 attributes: &[ElementAttribute {
1041 name: Some("subs"),
1042 value: "quotes",
1043 shorthand_items: &[],
1044 },],
1045 anchor: None,
1046 source: Span {
1047 data: "subs=quotes",
1048 line: 1,
1049 col: 2,
1050 offset: 1,
1051 },
1052 },),
1053 substitution_group: SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
1054 }
1055 );
1056
1057 assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
1058 assert_eq!(mi.item.raw_context().as_ref(), "listing");
1059 assert_eq!(mi.item.resolved_context().as_ref(), "listing");
1060 assert!(mi.item.declared_style().is_none());
1061 assert!(mi.item.id().is_none());
1062 assert!(mi.item.roles().is_empty());
1063 assert!(mi.item.options().is_empty());
1064 assert!(mi.item.title_source().is_none());
1065 assert!(mi.item.title().is_none());
1066 assert!(mi.item.anchor().is_none());
1067 assert!(mi.item.anchor_reftext().is_none());
1068
1069 assert_eq!(
1070 mi.item.attrlist().unwrap(),
1071 Attrlist {
1072 attributes: &[ElementAttribute {
1073 name: Some("subs"),
1074 value: "quotes",
1075 shorthand_items: &[],
1076 },],
1077 anchor: None,
1078 source: Span {
1079 data: "subs=quotes",
1080 line: 1,
1081 col: 2,
1082 offset: 1,
1083 },
1084 }
1085 );
1086
1087 assert_eq!(
1088 mi.item.substitution_group(),
1089 SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes])
1090 );
1091
1092 assert_eq!(
1093 mi.item.content(),
1094 Content {
1095 original: Span {
1096 data: "line1 < *line2*",
1097 line: 3,
1098 col: 1,
1099 offset: 19,
1100 },
1101 rendered: "line1 < <strong>line2</strong>",
1102 }
1103 );
1104 }
1105
1106 #[test]
1107 fn ignores_delimiter_prefix() {
1108 let mut parser = Parser::default();
1109
1110 let maw = crate::blocks::RawDelimitedBlock::parse(
1111 &BlockMetadata::new("----\nline1 \n-----\nline2\n----"),
1112 &mut parser,
1113 )
1114 .unwrap();
1115
1116 let mi = maw.item.unwrap().clone();
1117
1118 assert_eq!(
1119 mi.item,
1120 RawDelimitedBlock {
1121 content: Content {
1122 original: Span {
1123 data: "line1 \n-----\nline2",
1124 line: 2,
1125 col: 1,
1126 offset: 5,
1127 },
1128 rendered: "line1 \n-----\nline2",
1129 },
1130 content_model: ContentModel::Verbatim,
1131 context: "listing",
1132 source: Span {
1133 data: "----\nline1 \n-----\nline2\n----",
1134 line: 1,
1135 col: 1,
1136 offset: 0,
1137 },
1138 title_source: None,
1139 title: None,
1140 caption: None,
1141 number: None,
1142 anchor: None,
1143 anchor_reftext: None,
1144 attrlist: None,
1145 substitution_group: SubstitutionGroup::Verbatim,
1146 }
1147 );
1148
1149 assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
1150 assert_eq!(mi.item.raw_context().as_ref(), "listing");
1151 assert_eq!(mi.item.resolved_context().as_ref(), "listing");
1152 assert!(mi.item.declared_style().is_none());
1153 assert!(mi.item.id().is_none());
1154 assert!(mi.item.roles().is_empty());
1155 assert!(mi.item.options().is_empty());
1156 assert!(mi.item.title_source().is_none());
1157 assert!(mi.item.title().is_none());
1158 assert!(mi.item.anchor().is_none());
1159 assert!(mi.item.anchor_reftext().is_none());
1160 assert!(mi.item.attrlist().is_none());
1161 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
1162
1163 assert_eq!(
1164 mi.item.content(),
1165 Content {
1166 original: Span {
1167 data: "line1 \n-----\nline2",
1168 line: 2,
1169 col: 1,
1170 offset: 5,
1171 },
1172 rendered: "line1 \n-----\nline2",
1173 }
1174 );
1175
1176 assert_eq!(
1177 mi.item.content(),
1178 Content {
1179 original: Span {
1180 data: "line1 \n-----\nline2",
1181 line: 2,
1182 col: 1,
1183 offset: 5,
1184 },
1185 rendered: "line1 \n-----\nline2",
1186 }
1187 );
1188 }
1189 }
1190
1191 mod sidebar {
1192 use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1193
1194 #[test]
1195 fn empty() {
1196 let mut parser = Parser::default();
1197 assert!(
1198 crate::blocks::RawDelimitedBlock::parse(
1199 &BlockMetadata::new("****\n****"),
1200 &mut parser
1201 )
1202 .is_none()
1203 );
1204 }
1205
1206 #[test]
1207 fn multiple_lines() {
1208 let mut parser = Parser::default();
1209 assert!(
1210 crate::blocks::RawDelimitedBlock::parse(
1211 &BlockMetadata::new("****\nline1 \nline2\n****"),
1212 &mut parser
1213 )
1214 .is_none()
1215 );
1216 }
1217 }
1218
1219 mod table {
1220 use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1221
1222 #[test]
1223 fn empty() {
1224 let mut parser = Parser::default();
1225 assert!(
1226 crate::blocks::RawDelimitedBlock::parse(
1227 &BlockMetadata::new("|===\n|==="),
1228 &mut parser
1229 )
1230 .is_none()
1231 );
1232
1233 let mut parser = Parser::default();
1234 assert!(
1235 crate::blocks::RawDelimitedBlock::parse(
1236 &BlockMetadata::new(",===\n,==="),
1237 &mut parser
1238 )
1239 .is_none()
1240 );
1241
1242 let mut parser = Parser::default();
1243 assert!(
1244 crate::blocks::RawDelimitedBlock::parse(
1245 &BlockMetadata::new(":===\n:==="),
1246 &mut parser
1247 )
1248 .is_none()
1249 );
1250
1251 let mut parser = Parser::default();
1252 assert!(
1253 crate::blocks::RawDelimitedBlock::parse(
1254 &BlockMetadata::new("!===\n!==="),
1255 &mut parser
1256 )
1257 .is_none()
1258 );
1259 }
1260
1261 #[test]
1262 fn multiple_lines() {
1263 let mut parser = Parser::default();
1264 assert!(
1265 crate::blocks::RawDelimitedBlock::parse(
1266 &BlockMetadata::new("|===\nline1 \nline2\n|==="),
1267 &mut parser
1268 )
1269 .is_none()
1270 );
1271
1272 let mut parser = Parser::default();
1273 assert!(
1274 crate::blocks::RawDelimitedBlock::parse(
1275 &BlockMetadata::new(",===\nline1 \nline2\n,==="),
1276 &mut parser
1277 )
1278 .is_none()
1279 );
1280
1281 let mut parser = Parser::default();
1282 assert!(
1283 crate::blocks::RawDelimitedBlock::parse(
1284 &BlockMetadata::new(":===\nline1 \nline2\n:==="),
1285 &mut parser
1286 )
1287 .is_none()
1288 );
1289
1290 let mut parser = Parser::default();
1291 assert!(
1292 crate::blocks::RawDelimitedBlock::parse(
1293 &BlockMetadata::new("!===\nline1 \nline2\n!==="),
1294 &mut parser
1295 )
1296 .is_none()
1297 );
1298 }
1299 }
1300
1301 mod pass {
1302 use crate::{
1303 blocks::{ContentModel, metadata::BlockMetadata},
1304 tests::prelude::*,
1305 };
1306
1307 #[test]
1308 fn empty() {
1309 let mut parser = Parser::default();
1310 let maw = crate::blocks::RawDelimitedBlock::parse(
1311 &BlockMetadata::new("++++\n++++"),
1312 &mut parser,
1313 )
1314 .unwrap();
1315
1316 let mi = maw.item.unwrap().clone();
1317
1318 assert_eq!(
1319 mi.item,
1320 RawDelimitedBlock {
1321 content: Content {
1322 original: Span {
1323 data: "",
1324 line: 2,
1325 col: 1,
1326 offset: 5,
1327 },
1328 rendered: "",
1329 },
1330 content_model: ContentModel::Raw,
1331 context: "pass",
1332 source: Span {
1333 data: "++++\n++++",
1334 line: 1,
1335 col: 1,
1336 offset: 0,
1337 },
1338 title_source: None,
1339 title: None,
1340 caption: None,
1341 number: 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.content().is_empty());
1354 assert!(mi.item.id().is_none());
1355 assert!(mi.item.roles().is_empty());
1356 assert!(mi.item.options().is_empty());
1357 assert!(mi.item.title_source().is_none());
1358 assert!(mi.item.title().is_none());
1359 assert!(mi.item.anchor().is_none());
1360 assert!(mi.item.anchor_reftext().is_none());
1361 assert!(mi.item.attrlist().is_none());
1362 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1363 }
1364
1365 #[test]
1366 fn multiple_lines() {
1367 let mut parser = Parser::default();
1368
1369 let maw = crate::blocks::RawDelimitedBlock::parse(
1370 &BlockMetadata::new("++++\nline1 \nline2\n++++"),
1371 &mut parser,
1372 )
1373 .unwrap();
1374
1375 let mi = maw.item.unwrap().clone();
1376
1377 assert_eq!(
1378 mi.item,
1379 RawDelimitedBlock {
1380 content: Content {
1381 original: Span {
1382 data: "line1 \nline2",
1383 line: 2,
1384 col: 1,
1385 offset: 5,
1386 },
1387 rendered: "line1 \nline2",
1388 },
1389 content_model: ContentModel::Raw,
1390 context: "pass",
1391 source: Span {
1392 data: "++++\nline1 \nline2\n++++",
1393 line: 1,
1394 col: 1,
1395 offset: 0,
1396 },
1397 title_source: None,
1398 title: None,
1399 caption: None,
1400 number: None,
1401 anchor: None,
1402 anchor_reftext: None,
1403 attrlist: None,
1404 substitution_group: SubstitutionGroup::Pass,
1405 }
1406 );
1407
1408 assert_eq!(mi.item.content_model(), ContentModel::Raw);
1409 assert_eq!(mi.item.raw_context().as_ref(), "pass");
1410 assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1411 assert!(mi.item.declared_style().is_none());
1412 assert!(mi.item.id().is_none());
1413 assert!(mi.item.roles().is_empty());
1414 assert!(mi.item.options().is_empty());
1415 assert!(mi.item.title_source().is_none());
1416 assert!(mi.item.title().is_none());
1417 assert!(mi.item.anchor().is_none());
1418 assert!(mi.item.anchor_reftext().is_none());
1419 assert!(mi.item.attrlist().is_none());
1420 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1421
1422 assert_eq!(
1423 mi.item.content(),
1424 Content {
1425 original: Span {
1426 data: "line1 \nline2",
1427 line: 2,
1428 col: 1,
1429 offset: 5,
1430 },
1431 rendered: "line1 \nline2",
1432 }
1433 );
1434 }
1435
1436 #[test]
1437 fn ignores_delimiter_prefix() {
1438 let mut parser = Parser::default();
1439
1440 let maw = crate::blocks::RawDelimitedBlock::parse(
1441 &BlockMetadata::new("++++\nline1 \n+++++\nline2\n++++"),
1442 &mut parser,
1443 )
1444 .unwrap();
1445
1446 let mi = maw.item.unwrap().clone();
1447
1448 assert_eq!(
1449 mi.item,
1450 RawDelimitedBlock {
1451 content: Content {
1452 original: Span {
1453 data: "line1 \n+++++\nline2",
1454 line: 2,
1455 col: 1,
1456 offset: 5,
1457 },
1458 rendered: "line1 \n+++++\nline2",
1459 },
1460 content_model: ContentModel::Raw,
1461 context: "pass",
1462 source: Span {
1463 data: "++++\nline1 \n+++++\nline2\n++++",
1464 line: 1,
1465 col: 1,
1466 offset: 0,
1467 },
1468 title_source: None,
1469 title: None,
1470 caption: None,
1471 number: None,
1472 anchor: None,
1473 anchor_reftext: None,
1474 attrlist: None,
1475 substitution_group: SubstitutionGroup::Pass,
1476 }
1477 );
1478
1479 assert_eq!(mi.item.content_model(), ContentModel::Raw);
1480 assert_eq!(mi.item.raw_context().as_ref(), "pass");
1481 assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1482 assert!(mi.item.declared_style().is_none());
1483 assert!(mi.item.id().is_none());
1484 assert!(mi.item.roles().is_empty());
1485 assert!(mi.item.options().is_empty());
1486 assert!(mi.item.title_source().is_none());
1487 assert!(mi.item.title().is_none());
1488 assert!(mi.item.anchor().is_none());
1489 assert!(mi.item.anchor_reftext().is_none());
1490 assert!(mi.item.attrlist().is_none());
1491 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1492
1493 assert_eq!(
1494 mi.item.content(),
1495 Content {
1496 original: Span {
1497 data: "line1 \n+++++\nline2",
1498 line: 2,
1499 col: 1,
1500 offset: 5,
1501 },
1502 rendered: "line1 \n+++++\nline2",
1503 }
1504 );
1505 }
1506 }
1507
1508 mod stem {
1509 use crate::{blocks::ContentModel, tests::prelude::*};
1510
1511 #[test]
1516 fn stem_style_block() {
1517 let doc = Parser::default().parse("[stem]\n++++\na < b\n++++");
1518 let block = doc.nested_blocks().next().unwrap();
1519
1520 assert_eq!(block.content_model(), ContentModel::Raw);
1521 assert_eq!(block.raw_context().as_ref(), "stem");
1522 assert_eq!(block.resolved_context().as_ref(), "stem");
1523 assert_eq!(block.declared_style(), Some("stem"));
1524 assert_eq!(block.rendered_content(), Some("a < b"));
1525 assert_eq!(block.substitution_group(), SubstitutionGroup::Stem);
1526 assert!(doc.warnings().next().is_none());
1527 }
1528
1529 #[test]
1530 fn asciimath_style_block() {
1531 let doc = Parser::default().parse("[asciimath]\n++++\nx^2\n++++");
1532 let block = doc.nested_blocks().next().unwrap();
1533
1534 assert_eq!(block.raw_context().as_ref(), "stem");
1535 assert_eq!(block.declared_style(), Some("asciimath"));
1536 assert_eq!(block.rendered_content(), Some("x^2"));
1537 }
1538
1539 #[test]
1540 fn latexmath_style_block() {
1541 let doc = Parser::default().parse("[latexmath]\n++++\nC = \\alpha\n++++");
1542 let block = doc.nested_blocks().next().unwrap();
1543
1544 assert_eq!(block.raw_context().as_ref(), "stem");
1545 assert_eq!(block.declared_style(), Some("latexmath"));
1546 assert_eq!(block.rendered_content(), Some(r"C = \alpha"));
1547 }
1548
1549 #[test]
1552 fn unstyled_block_is_still_pass() {
1553 let doc = Parser::default().parse("++++\na < b\n++++");
1554 let block = doc.nested_blocks().next().unwrap();
1555
1556 assert_eq!(block.raw_context().as_ref(), "pass");
1557 assert_eq!(block.rendered_content(), Some("a < b"));
1558 assert_eq!(block.substitution_group(), SubstitutionGroup::Pass);
1559 }
1560
1561 #[test]
1564 fn subs_attribute_overrides_stem_default() {
1565 let doc = Parser::default().parse("[stem,subs=none]\n++++\na < b\n++++");
1566 let block = doc.nested_blocks().next().unwrap();
1567
1568 assert_eq!(block.raw_context().as_ref(), "stem");
1569 assert_eq!(block.rendered_content(), Some("a < b"));
1570 assert_eq!(block.substitution_group(), SubstitutionGroup::None);
1571 }
1572 }
1573
1574 mod quote {
1575 use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1576
1577 #[test]
1578 fn empty() {
1579 let mut parser = Parser::default();
1580 assert!(
1581 crate::blocks::RawDelimitedBlock::parse(
1582 &BlockMetadata::new("____\n____"),
1583 &mut parser
1584 )
1585 .is_none()
1586 );
1587 }
1588
1589 #[test]
1590 fn multiple_lines() {
1591 let mut parser = Parser::default();
1592 assert!(
1593 crate::blocks::RawDelimitedBlock::parse(
1594 &BlockMetadata::new("____\nline1 \nline2\n____"),
1595 &mut parser
1596 )
1597 .is_none()
1598 );
1599 }
1600 }
1601}