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