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<Content<'src>>,
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 title_content_mut(&mut self) -> Option<&mut Content<'src>> {
73 self.title.as_mut()
74 }
75
76 pub(crate) fn parse(
77 metadata: &BlockMetadata<'src>,
78 parser: &mut Parser,
79 ) -> Option<MatchedItem<'src, Self>> {
80 let MatchedItem {
81 item: (content, style),
82 after,
83 } = parse_lines(
84 metadata.block_start,
85 &metadata.attrlist,
86 false,
87 false,
88 false,
89 parser,
90 &[],
91 )?;
92
93 let caption = assign_block_caption(
98 parser,
99 "paragraph",
100 metadata.attrlist.as_ref(),
101 metadata.title.is_some(),
102 );
103 let number = caption.as_ref().and_then(|caption| caption.number);
104 let caption = caption.map(|caption| caption.prefix);
105
106 Some(MatchedItem {
107 item: Self {
108 content,
109 source: metadata
110 .source
111 .trim_remainder(after)
112 .trim_trailing_whitespace(),
113 style,
114 title_source: metadata.title_source,
115 title: metadata.title.clone(),
116 caption,
117 number,
118 anchor: metadata.anchor,
119 anchor_reftext: metadata.anchor_reftext,
120 attrlist: metadata.attrlist.clone(),
121 },
122 after: after.discard_empty_lines(),
123 })
124 }
125
126 pub(crate) fn parse_for_list_item(
127 metadata: &BlockMetadata<'src>,
128 parser: &mut Parser,
129 is_continuation: bool,
130 parent_list_markers: &[ListItemMarker<'src>],
131 ) -> Option<MatchedItem<'src, Self>> {
132 let MatchedItem {
133 item: (content, style),
134 after,
135 } = parse_lines(
136 metadata.block_start,
137 &metadata.attrlist,
138 true,
139 false,
140 is_continuation,
141 parser,
142 parent_list_markers,
143 )?;
144
145 let caption = assign_block_caption(
150 parser,
151 "paragraph",
152 metadata.attrlist.as_ref(),
153 metadata.title.is_some(),
154 );
155 let number = caption.as_ref().and_then(|caption| caption.number);
156 let caption = caption.map(|caption| caption.prefix);
157
158 Some(MatchedItem {
159 item: Self {
160 content,
161 source: metadata
162 .source
163 .trim_remainder(after)
164 .trim_trailing_whitespace(),
165 style,
166 title_source: metadata.title_source,
167 title: metadata.title.clone(),
168 caption,
169 number,
170 anchor: metadata.anchor,
171 anchor_reftext: metadata.anchor_reftext,
172 attrlist: metadata.attrlist.clone(),
173 },
174 after,
175 })
176 }
177
178 pub(crate) fn parse_for_definition_list(
183 metadata: &BlockMetadata<'src>,
184 parser: &mut Parser,
185 ) -> Option<MatchedItem<'src, Self>> {
186 let MatchedItem {
187 item: (content, style),
188 after,
189 } = parse_lines(
190 metadata.block_start,
191 &metadata.attrlist,
192 true,
193 true,
194 false,
195 parser,
196 &[],
197 )?;
198
199 let caption = assign_block_caption(
204 parser,
205 "paragraph",
206 metadata.attrlist.as_ref(),
207 metadata.title.is_some(),
208 );
209 let number = caption.as_ref().and_then(|caption| caption.number);
210 let caption = caption.map(|caption| caption.prefix);
211
212 Some(MatchedItem {
213 item: Self {
214 content,
215 source: metadata
216 .source
217 .trim_remainder(after)
218 .trim_trailing_whitespace(),
219 style,
220 title_source: metadata.title_source,
221 title: metadata.title.clone(),
222 caption,
223 number,
224 anchor: metadata.anchor,
225 anchor_reftext: metadata.anchor_reftext,
226 attrlist: metadata.attrlist.clone(),
227 },
228 after,
229 })
230 }
231
232 pub(crate) fn parse_fast(
233 source: Span<'src>,
234 parser: &Parser,
235 ) -> Option<MatchedItem<'src, Self>> {
236 let MatchedItem {
237 item: (content, style),
238 after,
239 } = parse_lines(source, &None, false, false, false, parser, &[])?;
240
241 let source = content.original();
242
243 Some(MatchedItem {
244 item: Self {
245 content,
246 source,
247 style,
248 title_source: None,
249 title: None,
250 caption: None,
251 number: None,
252 anchor: None,
253 anchor_reftext: None,
254 attrlist: None,
255 },
256 after: after.discard_empty_lines(),
257 })
258 }
259
260 pub fn content(&self) -> &Content<'src> {
262 &self.content
263 }
264
265 pub fn style(&self) -> SimpleBlockStyle {
267 self.style
268 }
269}
270
271fn parse_lines<'src>(
281 source: Span<'src>,
282 attrlist: &Option<Attrlist<'src>>,
283 mut stop_for_list_items: bool,
284 force_paragraph_style: bool,
285 preserve_literal_indent: bool,
286 parser: &Parser,
287 parent_list_markers: &[ListItemMarker<'src>],
288) -> Option<MatchedItem<'src, (Content<'src>, SimpleBlockStyle)>> {
289 let source_after_whitespace = source.discard_whitespace();
290 let first_line_indent = source_after_whitespace.col() - 1;
291
292 let mut indented_literal_mode = false;
295
296 let mut style = if source_after_whitespace.col() == source.col() || force_paragraph_style {
297 if source_after_whitespace.col() != source.col() {
300 indented_literal_mode = true;
301 }
302 SimpleBlockStyle::Paragraph
303 } else {
304 stop_for_list_items = false;
307 SimpleBlockStyle::Literal
308 };
309
310 if let Some(attrlist) = attrlist {
313 match attrlist.block_style() {
314 Some("normal") => {
315 style = SimpleBlockStyle::Paragraph;
316 }
317
318 Some("literal") => {
319 stop_for_list_items = false;
320 indented_literal_mode = false;
321 style = SimpleBlockStyle::Literal;
322 }
323
324 Some("listing") => {
325 stop_for_list_items = false;
326 indented_literal_mode = false;
327 style = SimpleBlockStyle::Listing;
328 }
329
330 Some("source") => {
331 stop_for_list_items = false;
332 indented_literal_mode = false;
333 style = SimpleBlockStyle::Source;
334 }
335
336 _ => {}
337 }
338 }
339
340 let comment_style = is_comment_style(attrlist.as_ref());
344
345 let mut next = source;
346 let mut filtered_lines: Vec<&'src str> = vec![];
347 let mut filtered_line_spans: Vec<Span<'src>> = vec![];
352 let mut skipped_comment_line = false;
353
354 let in_definition_list = parent_list_markers
359 .iter()
360 .any(|m| matches!(m, ListItemMarker::DefinedTerm { .. }));
361
362 let strip_indent =
363 if preserve_literal_indent && style == SimpleBlockStyle::Literal && in_definition_list {
364 let mut scan = source;
366 let mut min_indent = first_line_indent;
367 let mut line_count = 0;
368
369 while let Some(line_mi) = scan.take_non_empty_line() {
370 let line = line_mi.item;
371
372 if line_count > 0 && line.data() == "+" {
374 break;
375 }
376
377 if let Some(n) = line.position(|c| c != ' ' && c != '\t') {
378 min_indent = min_indent.min(n);
379 }
380
381 line_count += 1;
382 scan = line_mi.after;
383 }
384 min_indent
385 } else {
386 first_line_indent
387 };
388
389 while let Some(line_mi) = next.take_non_empty_line() {
390 let mut line = line_mi.item;
391
392 if !stop_for_list_items
396 && skipped_comment_line
397 && style == SimpleBlockStyle::Paragraph
398 && is_section_header(line.data(), parser.level_offset())
399 {
400 break;
401 }
402
403 if !filtered_lines.is_empty() {
408 let should_check_for_list_marker =
411 stop_for_list_items && (!indented_literal_mode || line.col() == 1);
412
413 if should_check_for_list_marker
417 && let Some(marker_mi) = ListItemMarker::parse(line, parser)
418 {
419 let is_ancestor_list = parent_list_markers
423 .iter()
424 .any(|p| p.is_match_for(&marker_mi.item));
425
426 if is_ancestor_list || !preserve_literal_indent {
427 break;
428 }
429 }
430
431 if line.data() == "+" {
432 break;
433 }
434
435 if line.starts_with('[') && line.ends_with(']') {
436 break;
437 }
438
439 if (line.starts_with('/')
440 || line.starts_with('-')
441 || line.starts_with('.')
442 || line.starts_with('+')
443 || line.starts_with('=')
444 || line.starts_with('*')
445 || line.starts_with('_')
446 || line.starts_with('`'))
447 && (RawDelimitedBlock::is_valid_delimiter(&line)
448 || CompoundDelimitedBlock::is_valid_delimiter(&line))
449 {
450 break;
451 }
452 }
453
454 next = line_mi.after;
455
456 if !comment_style
460 && style == SimpleBlockStyle::Paragraph
461 && line.starts_with("//")
462 && !line.starts_with("///")
463 {
464 skipped_comment_line = true;
465 continue;
466 }
467
468 let should_strip_indent = strip_indent > 0;
470
471 if should_strip_indent && let Some(n) = line.position(|c| c != ' ' && c != '\t') {
472 line = line.into_parse_result(n.min(strip_indent)).after;
473 };
474
475 let line = line.trim_trailing_whitespace();
476 filtered_line_spans.push(line);
477 filtered_lines.push(line.data());
478 }
479
480 let source = source.trim_remainder(next).trim_trailing_whitespace();
481 if source.is_empty() {
482 return None;
483 }
484
485 let mut content: Content<'src> =
486 Content::from_filtered_lines(source, &filtered_lines, filtered_line_spans);
487
488 let sub_group = if comment_style {
493 SubstitutionGroup::None
494 } else {
495 base_substitution_group(style).override_via_attrlist(attrlist.as_ref(), Some(parser))
496 };
497
498 sub_group.apply(&mut content, parser, attrlist.as_ref());
499
500 Some(MatchedItem {
501 item: (content, style),
502 after: next,
503 })
504}
505
506fn base_substitution_group(style: SimpleBlockStyle) -> SubstitutionGroup {
513 match style {
514 SimpleBlockStyle::Literal => SubstitutionGroup::Verbatim,
515 SimpleBlockStyle::Listing | SimpleBlockStyle::Source | SimpleBlockStyle::Paragraph => {
516 SubstitutionGroup::Normal
517 }
518 }
519}
520
521fn is_comment_style(attrlist: Option<&Attrlist<'_>>) -> bool {
525 attrlist.and_then(|attrlist| attrlist.block_style()) == Some("comment")
526}
527
528impl<'src> IsBlock<'src> for SimpleBlock<'src> {
529 fn content_model(&self) -> ContentModel {
530 ContentModel::Simple
531 }
532
533 fn content_mut(&mut self) -> Option<&mut Content<'src>> {
534 Some(&mut self.content)
535 }
536
537 fn rendered_content(&self) -> Option<&str> {
538 Some(self.content.rendered())
539 }
540
541 fn raw_context(&self) -> CowStr<'src> {
542 "paragraph".into()
543 }
544
545 fn title_source(&'src self) -> Option<Span<'src>> {
546 self.title_source
547 }
548
549 fn title(&self) -> Option<&str> {
550 self.title.as_ref().map(Content::rendered_str)
551 }
552
553 fn caption(&self) -> Option<&str> {
554 self.caption.as_deref()
555 }
556
557 fn number(&self) -> Option<usize> {
558 self.number
559 }
560
561 fn anchor(&'src self) -> Option<Span<'src>> {
562 self.anchor
563 }
564
565 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
566 self.anchor_reftext
567 }
568
569 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
570 self.attrlist.as_ref()
571 }
572
573 fn substitution_group(&'src self) -> SubstitutionGroup {
574 if is_comment_style(self.attrlist.as_ref()) {
579 SubstitutionGroup::None
580 } else {
581 base_substitution_group(self.style).override_via_attrlist(self.attrlist.as_ref(), None)
582 }
583 }
584}
585
586impl<'src> HasSpan<'src> for SimpleBlock<'src> {
587 fn span(&self) -> Span<'src> {
588 self.source
589 }
590}
591
592pub(crate) fn is_section_header(line: &str, level_offset: i32) -> bool {
605 let rest = if line.starts_with('=') {
607 line.trim_start_matches('=')
608 } else if line.starts_with('#') {
609 line.trim_start_matches('#')
610 } else {
611 return false;
612 };
613
614 let count = line.len() - rest.len();
618 if count == 0 || count > 6 || !rest.starts_with([' ', '\t']) {
619 return false;
620 }
621
622 let syntactic_level = (count - 1) as i32;
628 syntactic_level > 0 || syntactic_level.saturating_add(level_offset) >= 1
629}
630
631#[cfg(test)]
632mod tests {
633 #![allow(clippy::unwrap_used)]
634
635 use std::ops::Deref;
636
637 use crate::{
638 blocks::{ContentModel, SimpleBlockStyle, metadata::BlockMetadata},
639 tests::prelude::*,
640 };
641
642 #[test]
643 fn impl_clone() {
644 let mut parser = Parser::default();
646
647 let b1 =
648 crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
649
650 let b2 = b1.item.clone();
651 assert_eq!(b1.item, b2);
652 }
653
654 #[test]
655 fn style_enum_impl_debug() {
656 assert_eq!(
657 format!("{:?}", SimpleBlockStyle::Paragraph),
658 "SimpleBlockStyle::Paragraph"
659 );
660
661 assert_eq!(
662 format!("{:?}", SimpleBlockStyle::Literal),
663 "SimpleBlockStyle::Literal"
664 );
665
666 assert_eq!(
667 format!("{:?}", SimpleBlockStyle::Listing),
668 "SimpleBlockStyle::Listing"
669 );
670
671 assert_eq!(
672 format!("{:?}", SimpleBlockStyle::Source),
673 "SimpleBlockStyle::Source"
674 );
675 }
676
677 #[test]
678 fn empty_source() {
679 let mut parser = Parser::default();
680 assert!(crate::blocks::SimpleBlock::parse(&BlockMetadata::new(""), &mut parser).is_none());
681 }
682
683 #[test]
684 fn only_spaces() {
685 let mut parser = Parser::default();
686 assert!(
687 crate::blocks::SimpleBlock::parse(&BlockMetadata::new(" "), &mut parser).is_none()
688 );
689 }
690
691 #[test]
692 fn single_line() {
693 let mut parser = Parser::default();
694 let mi =
695 crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
696
697 assert_eq!(
698 mi.item,
699 SimpleBlock {
700 content: Content {
701 original: Span {
702 data: "abc",
703 line: 1,
704 col: 1,
705 offset: 0,
706 },
707 rendered: "abc",
708 },
709 source: Span {
710 data: "abc",
711 line: 1,
712 col: 1,
713 offset: 0,
714 },
715 style: SimpleBlockStyle::Paragraph,
716 title_source: None,
717 title: None,
718 caption: None,
719 number: None,
720 anchor: None,
721 anchor_reftext: None,
722 attrlist: None,
723 },
724 );
725
726 assert_eq!(mi.item.content_model(), ContentModel::Simple);
727 assert_eq!(mi.item.rendered_content().unwrap(), "abc");
728 assert_eq!(mi.item.raw_context().deref(), "paragraph");
729 assert_eq!(mi.item.resolved_context().deref(), "paragraph");
730 assert!(mi.item.declared_style().is_none());
731 assert!(mi.item.id().is_none());
732 assert!(mi.item.roles().is_empty());
733 assert!(mi.item.options().is_empty());
734 assert!(mi.item.title_source().is_none());
735 assert!(mi.item.title().is_none());
736 assert!(mi.item.anchor().is_none());
737 assert!(mi.item.anchor_reftext().is_none());
738 assert!(mi.item.attrlist().is_none());
739 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
740
741 assert_eq!(
742 mi.after,
743 Span {
744 data: "",
745 line: 1,
746 col: 4,
747 offset: 3
748 }
749 );
750 }
751
752 #[test]
753 fn multiple_lines() {
754 let mut parser = Parser::default();
755 let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\ndef"), &mut parser)
756 .unwrap();
757
758 assert_eq!(
759 mi.item,
760 SimpleBlock {
761 content: Content {
762 original: Span {
763 data: "abc\ndef",
764 line: 1,
765 col: 1,
766 offset: 0,
767 },
768 rendered: "abc\ndef",
769 },
770 source: Span {
771 data: "abc\ndef",
772 line: 1,
773 col: 1,
774 offset: 0,
775 },
776 style: SimpleBlockStyle::Paragraph,
777 title_source: None,
778 title: None,
779 caption: None,
780 number: None,
781 anchor: None,
782 anchor_reftext: None,
783 attrlist: None,
784 }
785 );
786
787 assert_eq!(
788 mi.after,
789 Span {
790 data: "",
791 line: 2,
792 col: 4,
793 offset: 7
794 }
795 );
796
797 assert_eq!(mi.item.rendered_content().unwrap(), "abc\ndef");
798 }
799
800 #[test]
801 fn consumes_blank_lines_after() {
802 let mut parser = Parser::default();
803 let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\n\ndef"), &mut parser)
804 .unwrap();
805
806 assert_eq!(
807 mi.item,
808 SimpleBlock {
809 content: Content {
810 original: Span {
811 data: "abc",
812 line: 1,
813 col: 1,
814 offset: 0,
815 },
816 rendered: "abc",
817 },
818 source: Span {
819 data: "abc",
820 line: 1,
821 col: 1,
822 offset: 0,
823 },
824 style: SimpleBlockStyle::Paragraph,
825 title_source: None,
826 title: None,
827 caption: None,
828 number: None,
829 anchor: None,
830 anchor_reftext: None,
831 attrlist: None,
832 }
833 );
834
835 assert_eq!(
836 mi.after,
837 Span {
838 data: "def",
839 line: 3,
840 col: 1,
841 offset: 5
842 }
843 );
844 }
845
846 #[test]
847 fn overrides_sub_group_via_subs_attribute() {
848 let mut parser = Parser::default();
849 let mi = crate::blocks::SimpleBlock::parse(
850 &BlockMetadata::new("[subs=quotes]\na<b>c *bold*\n\ndef"),
851 &mut parser,
852 )
853 .unwrap();
854
855 assert_eq!(
856 mi.item,
857 SimpleBlock {
858 content: Content {
859 original: Span {
860 data: "a<b>c *bold*",
861 line: 2,
862 col: 1,
863 offset: 14,
864 },
865 rendered: "a<b>c <strong>bold</strong>",
866 },
867 source: Span {
868 data: "[subs=quotes]\na<b>c *bold*",
869 line: 1,
870 col: 1,
871 offset: 0,
872 },
873 style: SimpleBlockStyle::Paragraph,
874 title_source: None,
875 title: None,
876 caption: None,
877 number: None,
878 anchor: None,
879 anchor_reftext: None,
880 attrlist: Some(Attrlist {
881 attributes: &[ElementAttribute {
882 name: Some("subs"),
883 value: "quotes",
884 shorthand_items: &[],
885 },],
886 anchor: None,
887 source: Span {
888 data: "subs=quotes",
889 line: 1,
890 col: 2,
891 offset: 1,
892 },
893 },),
894 }
895 );
896
897 assert_eq!(
898 mi.after,
899 Span {
900 data: "def",
901 line: 4,
902 col: 1,
903 offset: 28
904 }
905 );
906
907 assert_eq!(
908 mi.item.rendered_content().unwrap(),
909 "a<b>c <strong>bold</strong>"
910 );
911 }
912
913 mod is_section_header {
914 use super::super::is_section_header;
915
916 #[test]
917 fn multi_marker_is_always_a_section_regardless_of_offset() {
918 assert!(is_section_header("== Section", 0));
922 assert!(is_section_header("=== Section", 0));
923 assert!(is_section_header("## Section", 0));
924 assert!(is_section_header("== Section", -1));
925 }
926
927 #[test]
928 fn single_marker_is_a_section_only_under_positive_offset() {
929 assert!(!is_section_header("= Title", 0));
932 assert!(!is_section_header("# Title", 0));
933 assert!(is_section_header("= Title", 1));
934 assert!(is_section_header("# Title", 1));
935 assert!(is_section_header("= Title", 2));
936 }
937
938 #[test]
939 fn requires_a_blank_after_the_marker() {
940 assert!(!is_section_header("==nospace", 0));
941 assert!(!is_section_header("=nospace", 1));
942 assert!(!is_section_header("##nospace", 0));
943 }
944
945 #[test]
946 fn accepts_a_tab_after_the_marker() {
947 assert!(is_section_header("==\tSection", 0));
951 assert!(is_section_header("=\tSection", 1));
952 assert!(!is_section_header("=\tSection", 0));
953 }
954
955 #[test]
956 fn non_marker_and_over_long_marker_are_not_sections() {
957 assert!(!is_section_header("paragraph", 1));
958 assert!(!is_section_header("======= Too deep", 0));
960 }
961 }
962}