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