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