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