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