1use crate::{
2 HasSpan, Parser, Span,
3 attributes::Attrlist,
4 blocks::{
5 CompoundDelimitedBlock, ContentModel, IsBlock, ListItemMarker, RawDelimitedBlock,
6 caption::assign_block_caption, metadata::BlockMetadata,
7 },
8 content::{Content, SubstitutionGroup},
9 span::MatchedItem,
10 strings::CowStr,
11};
12
13#[derive(Clone, Copy, Eq, PartialEq)]
15pub enum SimpleBlockStyle {
16 Paragraph,
18
19 Literal,
21
22 Listing,
30
31 Source,
35}
36
37impl std::fmt::Debug for SimpleBlockStyle {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 SimpleBlockStyle::Paragraph => write!(f, "SimpleBlockStyle::Paragraph"),
41 SimpleBlockStyle::Literal => write!(f, "SimpleBlockStyle::Literal"),
42 SimpleBlockStyle::Listing => write!(f, "SimpleBlockStyle::Listing"),
43 SimpleBlockStyle::Source => write!(f, "SimpleBlockStyle::Source"),
44 }
45 }
46}
47
48#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct SimpleBlock<'src> {
52 content: Content<'src>,
53 source: Span<'src>,
54 style: SimpleBlockStyle,
55 title_source: Option<Span<'src>>,
56 title: Option<String>,
57 caption: Option<String>,
58 number: Option<usize>,
59 anchor: Option<Span<'src>>,
60 anchor_reftext: Option<Span<'src>>,
61 attrlist: Option<Attrlist<'src>>,
62}
63
64impl<'src> SimpleBlock<'src> {
65 pub(crate) fn parse(
66 metadata: &BlockMetadata<'src>,
67 parser: &mut Parser,
68 ) -> Option<MatchedItem<'src, Self>> {
69 let MatchedItem {
70 item: (content, style),
71 after,
72 } = parse_lines(
73 metadata.block_start,
74 &metadata.attrlist,
75 false,
76 false,
77 false,
78 parser,
79 &[],
80 )?;
81
82 let caption = assign_block_caption(
87 parser,
88 "paragraph",
89 metadata.attrlist.as_ref(),
90 metadata.title.is_some(),
91 );
92 let number = caption.as_ref().and_then(|caption| caption.number);
93 let caption = caption.map(|caption| caption.prefix);
94
95 Some(MatchedItem {
96 item: Self {
97 content,
98 source: metadata
99 .source
100 .trim_remainder(after)
101 .trim_trailing_whitespace(),
102 style,
103 title_source: metadata.title_source,
104 title: metadata.title.clone(),
105 caption,
106 number,
107 anchor: metadata.anchor,
108 anchor_reftext: metadata.anchor_reftext,
109 attrlist: metadata.attrlist.clone(),
110 },
111 after: after.discard_empty_lines(),
112 })
113 }
114
115 pub(crate) fn parse_for_list_item(
116 metadata: &BlockMetadata<'src>,
117 parser: &mut Parser,
118 is_continuation: bool,
119 parent_list_markers: &[ListItemMarker<'src>],
120 ) -> Option<MatchedItem<'src, Self>> {
121 let MatchedItem {
122 item: (content, style),
123 after,
124 } = parse_lines(
125 metadata.block_start,
126 &metadata.attrlist,
127 true,
128 false,
129 is_continuation,
130 parser,
131 parent_list_markers,
132 )?;
133
134 let caption = assign_block_caption(
139 parser,
140 "paragraph",
141 metadata.attrlist.as_ref(),
142 metadata.title.is_some(),
143 );
144 let number = caption.as_ref().and_then(|caption| caption.number);
145 let caption = caption.map(|caption| caption.prefix);
146
147 Some(MatchedItem {
148 item: Self {
149 content,
150 source: metadata
151 .source
152 .trim_remainder(after)
153 .trim_trailing_whitespace(),
154 style,
155 title_source: metadata.title_source,
156 title: metadata.title.clone(),
157 caption,
158 number,
159 anchor: metadata.anchor,
160 anchor_reftext: metadata.anchor_reftext,
161 attrlist: metadata.attrlist.clone(),
162 },
163 after,
164 })
165 }
166
167 pub(crate) fn parse_for_definition_list(
172 metadata: &BlockMetadata<'src>,
173 parser: &mut Parser,
174 ) -> Option<MatchedItem<'src, Self>> {
175 let MatchedItem {
176 item: (content, style),
177 after,
178 } = parse_lines(
179 metadata.block_start,
180 &metadata.attrlist,
181 true,
182 true,
183 false,
184 parser,
185 &[],
186 )?;
187
188 let caption = assign_block_caption(
193 parser,
194 "paragraph",
195 metadata.attrlist.as_ref(),
196 metadata.title.is_some(),
197 );
198 let number = caption.as_ref().and_then(|caption| caption.number);
199 let caption = caption.map(|caption| caption.prefix);
200
201 Some(MatchedItem {
202 item: Self {
203 content,
204 source: metadata
205 .source
206 .trim_remainder(after)
207 .trim_trailing_whitespace(),
208 style,
209 title_source: metadata.title_source,
210 title: metadata.title.clone(),
211 caption,
212 number,
213 anchor: metadata.anchor,
214 anchor_reftext: metadata.anchor_reftext,
215 attrlist: metadata.attrlist.clone(),
216 },
217 after,
218 })
219 }
220
221 pub(crate) fn parse_fast(
222 source: Span<'src>,
223 parser: &Parser,
224 ) -> Option<MatchedItem<'src, Self>> {
225 let MatchedItem {
226 item: (content, style),
227 after,
228 } = parse_lines(source, &None, false, false, false, parser, &[])?;
229
230 let source = content.original();
231
232 Some(MatchedItem {
233 item: Self {
234 content,
235 source,
236 style,
237 title_source: None,
238 title: None,
239 caption: None,
240 number: None,
241 anchor: None,
242 anchor_reftext: None,
243 attrlist: None,
244 },
245 after: after.discard_empty_lines(),
246 })
247 }
248
249 pub fn content(&self) -> &Content<'src> {
251 &self.content
252 }
253
254 pub fn style(&self) -> SimpleBlockStyle {
256 self.style
257 }
258}
259
260fn parse_lines<'src>(
270 source: Span<'src>,
271 attrlist: &Option<Attrlist<'src>>,
272 mut stop_for_list_items: bool,
273 force_paragraph_style: bool,
274 preserve_literal_indent: bool,
275 parser: &Parser,
276 parent_list_markers: &[ListItemMarker<'src>],
277) -> Option<MatchedItem<'src, (Content<'src>, SimpleBlockStyle)>> {
278 let source_after_whitespace = source.discard_whitespace();
279 let first_line_indent = source_after_whitespace.col() - 1;
280
281 let mut indented_literal_mode = false;
284
285 let mut style = if source_after_whitespace.col() == source.col() || force_paragraph_style {
286 if source_after_whitespace.col() != source.col() {
289 indented_literal_mode = true;
290 }
291 SimpleBlockStyle::Paragraph
292 } else {
293 stop_for_list_items = false;
296 SimpleBlockStyle::Literal
297 };
298
299 if let Some(attrlist) = attrlist {
302 match attrlist.block_style() {
303 Some("normal") => {
304 style = SimpleBlockStyle::Paragraph;
305 }
306
307 Some("literal") => {
308 stop_for_list_items = false;
309 indented_literal_mode = false;
310 style = SimpleBlockStyle::Literal;
311 }
312
313 Some("listing") => {
314 stop_for_list_items = false;
315 indented_literal_mode = false;
316 style = SimpleBlockStyle::Listing;
317 }
318
319 Some("source") => {
320 stop_for_list_items = false;
321 indented_literal_mode = false;
322 style = SimpleBlockStyle::Source;
323 }
324
325 _ => {}
326 }
327 }
328
329 let comment_style = is_comment_style(attrlist.as_ref());
333
334 let mut next = source;
335 let mut filtered_lines: Vec<&'src str> = vec![];
336 let mut skipped_comment_line = false;
337
338 let in_definition_list = parent_list_markers
343 .iter()
344 .any(|m| matches!(m, ListItemMarker::DefinedTerm { .. }));
345
346 let strip_indent =
347 if preserve_literal_indent && style == SimpleBlockStyle::Literal && in_definition_list {
348 let mut scan = source;
350 let mut min_indent = first_line_indent;
351 let mut line_count = 0;
352
353 while let Some(line_mi) = scan.take_non_empty_line() {
354 let line = line_mi.item;
355
356 if line_count > 0 && line.data() == "+" {
358 break;
359 }
360
361 if let Some(n) = line.position(|c| c != ' ' && c != '\t') {
362 min_indent = min_indent.min(n);
363 }
364
365 line_count += 1;
366 scan = line_mi.after;
367 }
368 min_indent
369 } else {
370 first_line_indent
371 };
372
373 while let Some(line_mi) = next.take_non_empty_line() {
374 let mut line = line_mi.item;
375
376 if !stop_for_list_items
380 && skipped_comment_line
381 && style == SimpleBlockStyle::Paragraph
382 && is_section_header(line.data())
383 {
384 break;
385 }
386
387 if !filtered_lines.is_empty() {
392 let should_check_for_list_marker =
395 stop_for_list_items && (!indented_literal_mode || line.col() == 1);
396
397 if should_check_for_list_marker
401 && let Some(marker_mi) = ListItemMarker::parse(line, parser)
402 {
403 let is_ancestor_list = parent_list_markers
407 .iter()
408 .any(|p| p.is_match_for(&marker_mi.item));
409
410 if is_ancestor_list || !preserve_literal_indent {
411 break;
412 }
413 }
414
415 if line.data() == "+" {
416 break;
417 }
418
419 if line.starts_with('[') && line.ends_with(']') {
420 break;
421 }
422
423 if (line.starts_with('/')
424 || line.starts_with('-')
425 || line.starts_with('.')
426 || line.starts_with('+')
427 || line.starts_with('=')
428 || line.starts_with('*')
429 || line.starts_with('_')
430 || line.starts_with('`'))
431 && (RawDelimitedBlock::is_valid_delimiter(&line)
432 || CompoundDelimitedBlock::is_valid_delimiter(&line))
433 {
434 break;
435 }
436 }
437
438 next = line_mi.after;
439
440 if !comment_style
444 && style == SimpleBlockStyle::Paragraph
445 && line.starts_with("//")
446 && !line.starts_with("///")
447 {
448 skipped_comment_line = true;
449 continue;
450 }
451
452 let should_strip_indent = strip_indent > 0;
454
455 if should_strip_indent && let Some(n) = line.position(|c| c != ' ' && c != '\t') {
456 line = line.into_parse_result(n.min(strip_indent)).after;
457 };
458
459 filtered_lines.push(line.trim_trailing_whitespace().data());
460 }
461
462 let source = source.trim_remainder(next).trim_trailing_whitespace();
463 if source.is_empty() {
464 return None;
465 }
466
467 let filtered_lines = filtered_lines.join("\n");
468 let mut content: Content<'src> = Content::from_filtered(source, filtered_lines);
469
470 let sub_group = if comment_style {
475 SubstitutionGroup::None
476 } else {
477 base_substitution_group(style).override_via_attrlist(attrlist.as_ref())
478 };
479
480 sub_group.apply(&mut content, parser, attrlist.as_ref());
481
482 Some(MatchedItem {
483 item: (content, style),
484 after: next,
485 })
486}
487
488fn base_substitution_group(style: SimpleBlockStyle) -> SubstitutionGroup {
495 match style {
496 SimpleBlockStyle::Literal => SubstitutionGroup::Verbatim,
497 SimpleBlockStyle::Listing | SimpleBlockStyle::Source | SimpleBlockStyle::Paragraph => {
498 SubstitutionGroup::Normal
499 }
500 }
501}
502
503fn is_comment_style(attrlist: Option<&Attrlist<'_>>) -> bool {
507 attrlist.and_then(|attrlist| attrlist.block_style()) == Some("comment")
508}
509
510impl<'src> IsBlock<'src> for SimpleBlock<'src> {
511 fn content_model(&self) -> ContentModel {
512 ContentModel::Simple
513 }
514
515 fn content_mut(&mut self) -> Option<&mut Content<'src>> {
516 Some(&mut self.content)
517 }
518
519 fn rendered_content(&self) -> Option<&str> {
520 Some(self.content.rendered())
521 }
522
523 fn raw_context(&self) -> CowStr<'src> {
524 "paragraph".into()
525 }
526
527 fn title_source(&'src self) -> Option<Span<'src>> {
528 self.title_source
529 }
530
531 fn title(&self) -> Option<&str> {
532 self.title.as_deref()
533 }
534
535 fn caption(&self) -> Option<&str> {
536 self.caption.as_deref()
537 }
538
539 fn number(&self) -> Option<usize> {
540 self.number
541 }
542
543 fn anchor(&'src self) -> Option<Span<'src>> {
544 self.anchor
545 }
546
547 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
548 self.anchor_reftext
549 }
550
551 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
552 self.attrlist.as_ref()
553 }
554
555 fn substitution_group(&'src self) -> SubstitutionGroup {
556 if is_comment_style(self.attrlist.as_ref()) {
561 SubstitutionGroup::None
562 } else {
563 base_substitution_group(self.style).override_via_attrlist(self.attrlist.as_ref())
564 }
565 }
566}
567
568impl<'src> HasSpan<'src> for SimpleBlock<'src> {
569 fn span(&self) -> Span<'src> {
570 self.source
571 }
572}
573
574fn is_section_header(line: &str) -> bool {
577 if line.starts_with("==") {
579 let rest = line.trim_start_matches('=');
580 if rest.starts_with(' ') {
581 return true;
582 }
583 }
584
585 if line.starts_with("##") {
587 let rest = line.trim_start_matches('#');
588 if rest.starts_with(' ') {
589 return true;
590 }
591 }
592
593 false
594}
595
596#[cfg(test)]
597mod tests {
598 #![allow(clippy::unwrap_used)]
599
600 use std::ops::Deref;
601
602 use crate::{
603 blocks::{ContentModel, SimpleBlockStyle, metadata::BlockMetadata},
604 tests::prelude::*,
605 };
606
607 #[test]
608 fn impl_clone() {
609 let mut parser = Parser::default();
611
612 let b1 =
613 crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
614
615 let b2 = b1.item.clone();
616 assert_eq!(b1.item, b2);
617 }
618
619 #[test]
620 fn style_enum_impl_debug() {
621 assert_eq!(
622 format!("{:?}", SimpleBlockStyle::Paragraph),
623 "SimpleBlockStyle::Paragraph"
624 );
625
626 assert_eq!(
627 format!("{:?}", SimpleBlockStyle::Literal),
628 "SimpleBlockStyle::Literal"
629 );
630
631 assert_eq!(
632 format!("{:?}", SimpleBlockStyle::Listing),
633 "SimpleBlockStyle::Listing"
634 );
635
636 assert_eq!(
637 format!("{:?}", SimpleBlockStyle::Source),
638 "SimpleBlockStyle::Source"
639 );
640 }
641
642 #[test]
643 fn empty_source() {
644 let mut parser = Parser::default();
645 assert!(crate::blocks::SimpleBlock::parse(&BlockMetadata::new(""), &mut parser).is_none());
646 }
647
648 #[test]
649 fn only_spaces() {
650 let mut parser = Parser::default();
651 assert!(
652 crate::blocks::SimpleBlock::parse(&BlockMetadata::new(" "), &mut parser).is_none()
653 );
654 }
655
656 #[test]
657 fn single_line() {
658 let mut parser = Parser::default();
659 let mi =
660 crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
661
662 assert_eq!(
663 mi.item,
664 SimpleBlock {
665 content: Content {
666 original: Span {
667 data: "abc",
668 line: 1,
669 col: 1,
670 offset: 0,
671 },
672 rendered: "abc",
673 },
674 source: Span {
675 data: "abc",
676 line: 1,
677 col: 1,
678 offset: 0,
679 },
680 style: SimpleBlockStyle::Paragraph,
681 title_source: None,
682 title: None,
683 caption: None,
684 number: None,
685 anchor: None,
686 anchor_reftext: None,
687 attrlist: None,
688 },
689 );
690
691 assert_eq!(mi.item.content_model(), ContentModel::Simple);
692 assert_eq!(mi.item.rendered_content().unwrap(), "abc");
693 assert_eq!(mi.item.raw_context().deref(), "paragraph");
694 assert_eq!(mi.item.resolved_context().deref(), "paragraph");
695 assert!(mi.item.declared_style().is_none());
696 assert!(mi.item.id().is_none());
697 assert!(mi.item.roles().is_empty());
698 assert!(mi.item.options().is_empty());
699 assert!(mi.item.title_source().is_none());
700 assert!(mi.item.title().is_none());
701 assert!(mi.item.anchor().is_none());
702 assert!(mi.item.anchor_reftext().is_none());
703 assert!(mi.item.attrlist().is_none());
704 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
705
706 assert_eq!(
707 mi.after,
708 Span {
709 data: "",
710 line: 1,
711 col: 4,
712 offset: 3
713 }
714 );
715 }
716
717 #[test]
718 fn multiple_lines() {
719 let mut parser = Parser::default();
720 let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\ndef"), &mut parser)
721 .unwrap();
722
723 assert_eq!(
724 mi.item,
725 SimpleBlock {
726 content: Content {
727 original: Span {
728 data: "abc\ndef",
729 line: 1,
730 col: 1,
731 offset: 0,
732 },
733 rendered: "abc\ndef",
734 },
735 source: Span {
736 data: "abc\ndef",
737 line: 1,
738 col: 1,
739 offset: 0,
740 },
741 style: SimpleBlockStyle::Paragraph,
742 title_source: None,
743 title: None,
744 caption: None,
745 number: None,
746 anchor: None,
747 anchor_reftext: None,
748 attrlist: None,
749 }
750 );
751
752 assert_eq!(
753 mi.after,
754 Span {
755 data: "",
756 line: 2,
757 col: 4,
758 offset: 7
759 }
760 );
761
762 assert_eq!(mi.item.rendered_content().unwrap(), "abc\ndef");
763 }
764
765 #[test]
766 fn consumes_blank_lines_after() {
767 let mut parser = Parser::default();
768 let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\n\ndef"), &mut parser)
769 .unwrap();
770
771 assert_eq!(
772 mi.item,
773 SimpleBlock {
774 content: Content {
775 original: Span {
776 data: "abc",
777 line: 1,
778 col: 1,
779 offset: 0,
780 },
781 rendered: "abc",
782 },
783 source: Span {
784 data: "abc",
785 line: 1,
786 col: 1,
787 offset: 0,
788 },
789 style: SimpleBlockStyle::Paragraph,
790 title_source: None,
791 title: None,
792 caption: None,
793 number: None,
794 anchor: None,
795 anchor_reftext: None,
796 attrlist: None,
797 }
798 );
799
800 assert_eq!(
801 mi.after,
802 Span {
803 data: "def",
804 line: 3,
805 col: 1,
806 offset: 5
807 }
808 );
809 }
810
811 #[test]
812 fn overrides_sub_group_via_subs_attribute() {
813 let mut parser = Parser::default();
814 let mi = crate::blocks::SimpleBlock::parse(
815 &BlockMetadata::new("[subs=quotes]\na<b>c *bold*\n\ndef"),
816 &mut parser,
817 )
818 .unwrap();
819
820 assert_eq!(
821 mi.item,
822 SimpleBlock {
823 content: Content {
824 original: Span {
825 data: "a<b>c *bold*",
826 line: 2,
827 col: 1,
828 offset: 14,
829 },
830 rendered: "a<b>c <strong>bold</strong>",
831 },
832 source: Span {
833 data: "[subs=quotes]\na<b>c *bold*",
834 line: 1,
835 col: 1,
836 offset: 0,
837 },
838 style: SimpleBlockStyle::Paragraph,
839 title_source: None,
840 title: None,
841 caption: None,
842 number: None,
843 anchor: None,
844 anchor_reftext: None,
845 attrlist: Some(Attrlist {
846 attributes: &[ElementAttribute {
847 name: Some("subs"),
848 value: "quotes",
849 shorthand_items: &[],
850 },],
851 anchor: None,
852 source: Span {
853 data: "subs=quotes",
854 line: 1,
855 col: 2,
856 offset: 1,
857 },
858 },),
859 }
860 );
861
862 assert_eq!(
863 mi.after,
864 Span {
865 data: "def",
866 line: 4,
867 col: 1,
868 offset: 28
869 }
870 );
871
872 assert_eq!(
873 mi.item.rendered_content().unwrap(),
874 "a<b>c <strong>bold</strong>"
875 );
876 }
877}