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