1use crate::{
2 HasSpan, Parser, Span,
3 attributes::Attrlist,
4 blocks::{
5 ChildBlocks, CompoundDelimitedBlock, ContentModel, IsBlock, ListItemMarker,
6 RawDelimitedBlock, TableBlock,
7 caption::assign_block_caption,
8 metadata::{BlockMetadata, block_title_text},
9 },
10 content::{Content, SubstitutionGroup},
11 span::MatchedItem,
12 strings::CowStr,
13};
14
15#[derive(Clone, Copy, Eq, Hash, PartialEq)]
17pub enum SimpleBlockStyle {
18 Paragraph,
20
21 Literal,
23
24 Listing,
32
33 Source,
37}
38
39impl std::fmt::Debug for SimpleBlockStyle {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 SimpleBlockStyle::Paragraph => write!(f, "SimpleBlockStyle::Paragraph"),
43 SimpleBlockStyle::Literal => write!(f, "SimpleBlockStyle::Literal"),
44 SimpleBlockStyle::Listing => write!(f, "SimpleBlockStyle::Listing"),
45 SimpleBlockStyle::Source => write!(f, "SimpleBlockStyle::Source"),
46 }
47 }
48}
49
50#[derive(Clone, Debug, Eq, Hash, PartialEq)]
53pub struct SimpleBlock<'src> {
54 content: Content<'src>,
55 source: Span<'src>,
56 style: SimpleBlockStyle,
57 title_source: Option<Span<'src>>,
58 title: Option<Content<'src>>,
59 caption: Option<String>,
60 number: Option<usize>,
61 anchor: Option<Span<'src>>,
62 anchor_reftext: Option<Span<'src>>,
63 attrlist: Option<Attrlist<'src>>,
64}
65
66impl<'src> SimpleBlock<'src> {
67 pub fn child_blocks(&'src self) -> ChildBlocks<'src> {
73 ChildBlocks::empty()
74 }
75
76 pub(crate) fn title_content_mut(&mut self) -> Option<&mut Content<'src>> {
84 self.title.as_mut()
85 }
86
87 pub(crate) fn parse(
88 metadata: &BlockMetadata<'src>,
89 parser: &mut Parser,
90 ) -> Option<MatchedItem<'src, Self>> {
91 let MatchedItem {
92 item: (content, style),
93 after,
94 } = parse_lines(
95 metadata.block_start,
96 &metadata.attrlist,
97 false,
98 false,
99 false,
100 parser,
101 &[],
102 )?;
103
104 let caption = assign_block_caption(
109 parser,
110 "paragraph",
111 metadata.attrlist.as_ref(),
112 metadata.title.is_some(),
113 );
114 let number = caption.as_ref().and_then(|caption| caption.number);
115 let caption = caption.map(|caption| caption.prefix);
116
117 Some(MatchedItem {
118 item: Self {
119 content,
120 source: metadata
121 .source
122 .trim_remainder(after)
123 .trim_trailing_whitespace(),
124 style,
125 title_source: metadata.title_source,
126 title: metadata.title.clone(),
127 caption,
128 number,
129 anchor: metadata.anchor,
130 anchor_reftext: metadata.anchor_reftext,
131 attrlist: metadata.attrlist.clone(),
132 },
133 after: after.discard_empty_lines(),
134 })
135 }
136
137 pub(crate) fn parse_for_list_item(
138 metadata: &BlockMetadata<'src>,
139 parser: &mut Parser,
140 is_continuation: bool,
141 parent_list_markers: &[ListItemMarker<'src>],
142 ) -> Option<MatchedItem<'src, Self>> {
143 let MatchedItem {
144 item: (content, style),
145 after,
146 } = parse_lines(
147 metadata.block_start,
148 &metadata.attrlist,
149 true,
150 false,
151 is_continuation,
152 parser,
153 parent_list_markers,
154 )?;
155
156 let caption = assign_block_caption(
161 parser,
162 "paragraph",
163 metadata.attrlist.as_ref(),
164 metadata.title.is_some(),
165 );
166 let number = caption.as_ref().and_then(|caption| caption.number);
167 let caption = caption.map(|caption| caption.prefix);
168
169 Some(MatchedItem {
170 item: Self {
171 content,
172 source: metadata
173 .source
174 .trim_remainder(after)
175 .trim_trailing_whitespace(),
176 style,
177 title_source: metadata.title_source,
178 title: metadata.title.clone(),
179 caption,
180 number,
181 anchor: metadata.anchor,
182 anchor_reftext: metadata.anchor_reftext,
183 attrlist: metadata.attrlist.clone(),
184 },
185 after,
186 })
187 }
188
189 pub(crate) fn parse_for_definition_list(
194 metadata: &BlockMetadata<'src>,
195 parser: &mut Parser,
196 ) -> Option<MatchedItem<'src, Self>> {
197 let MatchedItem {
198 item: (content, style),
199 after,
200 } = parse_lines(
201 metadata.block_start,
202 &metadata.attrlist,
203 true,
204 true,
205 false,
206 parser,
207 &[],
208 )?;
209
210 let caption = assign_block_caption(
215 parser,
216 "paragraph",
217 metadata.attrlist.as_ref(),
218 metadata.title.is_some(),
219 );
220 let number = caption.as_ref().and_then(|caption| caption.number);
221 let caption = caption.map(|caption| caption.prefix);
222
223 Some(MatchedItem {
224 item: Self {
225 content,
226 source: metadata
227 .source
228 .trim_remainder(after)
229 .trim_trailing_whitespace(),
230 style,
231 title_source: metadata.title_source,
232 title: metadata.title.clone(),
233 caption,
234 number,
235 anchor: metadata.anchor,
236 anchor_reftext: metadata.anchor_reftext,
237 attrlist: metadata.attrlist.clone(),
238 },
239 after,
240 })
241 }
242
243 pub(crate) fn parse_fast(
244 source: Span<'src>,
245 parser: &Parser,
246 ) -> Option<MatchedItem<'src, Self>> {
247 let MatchedItem {
248 item: (content, style),
249 after,
250 } = parse_lines(source, &None, false, false, false, parser, &[])?;
251
252 let source = content.original();
253
254 Some(MatchedItem {
255 item: Self {
256 content,
257 source,
258 style,
259 title_source: None,
260 title: None,
261 caption: None,
262 number: None,
263 anchor: None,
264 anchor_reftext: None,
265 attrlist: None,
266 },
267 after: after.discard_empty_lines(),
268 })
269 }
270
271 pub fn content(&self) -> &Content<'src> {
273 &self.content
274 }
275
276 pub fn style(&self) -> SimpleBlockStyle {
278 self.style
279 }
280}
281
282fn parse_lines<'src>(
292 source: Span<'src>,
293 attrlist: &Option<Attrlist<'src>>,
294 mut stop_for_list_items: bool,
295 force_paragraph_style: bool,
296 preserve_literal_indent: bool,
297 parser: &Parser,
298 parent_list_markers: &[ListItemMarker<'src>],
299) -> Option<MatchedItem<'src, (Content<'src>, SimpleBlockStyle)>> {
300 let source_after_whitespace = source.discard_whitespace();
301 let first_line_indent = source_after_whitespace.col() - 1;
302
303 let mut indented_literal_mode = false;
306
307 let mut style = if source_after_whitespace.col() == source.col() || force_paragraph_style {
308 if source_after_whitespace.col() != source.col() {
311 indented_literal_mode = true;
312 }
313 SimpleBlockStyle::Paragraph
314 } else {
315 stop_for_list_items = false;
318 SimpleBlockStyle::Literal
319 };
320
321 if let Some(attrlist) = attrlist {
324 match attrlist.block_style() {
325 Some("normal") => {
326 style = SimpleBlockStyle::Paragraph;
327 }
328
329 Some("literal") => {
330 stop_for_list_items = false;
331 indented_literal_mode = false;
332 style = SimpleBlockStyle::Literal;
333 }
334
335 Some("listing") => {
336 stop_for_list_items = false;
337 indented_literal_mode = false;
338 style = SimpleBlockStyle::Listing;
339 }
340
341 Some("source") => {
342 stop_for_list_items = false;
343 indented_literal_mode = false;
344 style = SimpleBlockStyle::Source;
345 }
346
347 _ => {}
348 }
349 }
350
351 let comment_style = is_comment_style(attrlist.as_ref());
355
356 let mut next = source;
357 let mut filtered_lines: Vec<&'src str> = vec![];
358
359 let mut filtered_line_spans: Vec<Span<'src>> = vec![];
364 let mut skipped_comment_line = false;
365
366 let in_definition_list = parent_list_markers
371 .iter()
372 .any(|m| matches!(m, ListItemMarker::DefinedTerm { .. }));
373
374 let strip_indent =
375 if preserve_literal_indent && style == SimpleBlockStyle::Literal && in_definition_list {
376 let mut scan = source;
378 let mut min_indent = first_line_indent;
379 let mut line_count = 0;
380
381 while let Some(line_mi) = scan.take_non_empty_line() {
382 let line = line_mi.item;
383
384 if line_count > 0 && line.data() == "+" {
386 break;
387 }
388
389 if let Some(n) = line.position(|c| c != ' ' && c != '\t') {
390 min_indent = min_indent.min(n);
391 }
392
393 line_count += 1;
394 scan = line_mi.after;
395 }
396 min_indent
397 } else {
398 first_line_indent
399 };
400
401 while let Some(line_mi) = next.take_non_empty_line() {
402 let mut line = line_mi.item;
403
404 if !stop_for_list_items
408 && skipped_comment_line
409 && style == SimpleBlockStyle::Paragraph
410 && is_section_header(line.data(), parser.level_offset())
411 {
412 break;
413 }
414
415 if !stop_for_list_items
431 && skipped_comment_line
432 && filtered_lines.is_empty()
433 && style == SimpleBlockStyle::Paragraph
434 && (block_title_text(line).is_some()
435 || (line.starts_with('[') && line.ends_with(']'))
436 || RawDelimitedBlock::is_valid_delimiter(&line)
437 || CompoundDelimitedBlock::is_valid_delimiter(&line)
438 || TableBlock::is_table_delimiter(&line))
439 {
440 break;
441 }
442
443 if !filtered_lines.is_empty() {
448 let should_check_for_list_marker =
451 stop_for_list_items && (!indented_literal_mode || line.col() == 1);
452
453 if should_check_for_list_marker
457 && let Some(marker_mi) = ListItemMarker::parse(line, parser)
458 {
459 let is_ancestor_list = parent_list_markers
463 .iter()
464 .any(|p| p.is_match_for(&marker_mi.item));
465
466 if is_ancestor_list || !preserve_literal_indent {
467 break;
468 }
469 }
470
471 if line.data() == "+" {
472 break;
473 }
474
475 if line.starts_with('[') && line.ends_with(']') {
476 break;
477 }
478
479 if (line.starts_with('/')
480 || line.starts_with('-')
481 || line.starts_with('.')
482 || line.starts_with('+')
483 || line.starts_with('=')
484 || line.starts_with('*')
485 || line.starts_with('_')
486 || line.starts_with('`'))
487 && (RawDelimitedBlock::is_valid_delimiter(&line)
488 || CompoundDelimitedBlock::is_valid_delimiter(&line))
489 {
490 break;
491 }
492 }
493
494 next = line_mi.after;
495
496 if !comment_style
500 && style == SimpleBlockStyle::Paragraph
501 && line.starts_with("//")
502 && !line.starts_with("///")
503 {
504 skipped_comment_line = true;
505 continue;
506 }
507
508 let should_strip_indent = strip_indent > 0;
510
511 if should_strip_indent && let Some(n) = line.position(|c| c != ' ' && c != '\t') {
512 line = line.into_parse_result(n.min(strip_indent)).after;
513 };
514
515 let line = line.trim_trailing_whitespace();
516 filtered_line_spans.push(line);
517 filtered_lines.push(line.data());
518 }
519
520 let source = source.trim_remainder(next).trim_trailing_whitespace();
521 if source.is_empty() {
522 return None;
523 }
524
525 let mut content: Content<'src> =
526 Content::from_filtered_lines(source, &filtered_lines, filtered_line_spans);
527
528 let sub_group = if comment_style {
533 SubstitutionGroup::None
534 } else {
535 base_substitution_group(style).override_via_attrlist(attrlist.as_ref(), Some(parser))
536 };
537
538 sub_group.apply(&mut content, parser, attrlist.as_ref());
539
540 Some(MatchedItem {
541 item: (content, style),
542 after: next,
543 })
544}
545
546fn base_substitution_group(style: SimpleBlockStyle) -> SubstitutionGroup {
553 match style {
554 SimpleBlockStyle::Literal => SubstitutionGroup::Verbatim,
555 SimpleBlockStyle::Listing | SimpleBlockStyle::Source | SimpleBlockStyle::Paragraph => {
556 SubstitutionGroup::Normal
557 }
558 }
559}
560
561fn is_comment_style(attrlist: Option<&Attrlist<'_>>) -> bool {
565 attrlist.and_then(|attrlist| attrlist.block_style()) == Some("comment")
566}
567
568impl<'src> IsBlock<'src> for SimpleBlock<'src> {
569 fn content_model(&self) -> ContentModel {
570 ContentModel::Simple
571 }
572
573 fn content_mut(&mut self) -> Option<&mut Content<'src>> {
574 Some(&mut self.content)
575 }
576
577 fn rendered_content(&self) -> Option<&str> {
578 Some(self.content.rendered())
579 }
580
581 fn raw_context(&self) -> CowStr<'src> {
582 "paragraph".into()
583 }
584
585 fn title_source(&'src self) -> Option<Span<'src>> {
586 self.title_source
587 }
588
589 fn title(&self) -> Option<&str> {
590 self.title.as_ref().map(Content::rendered_str)
591 }
592
593 fn caption(&self) -> Option<&str> {
594 self.caption.as_deref()
595 }
596
597 fn number(&self) -> Option<usize> {
598 self.number
599 }
600
601 fn anchor(&'src self) -> Option<Span<'src>> {
602 self.anchor
603 }
604
605 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
606 self.anchor_reftext
607 }
608
609 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
610 self.attrlist.as_ref()
611 }
612
613 fn substitution_group(&'src self) -> SubstitutionGroup {
614 if is_comment_style(self.attrlist.as_ref()) {
619 SubstitutionGroup::None
620 } else {
621 base_substitution_group(self.style).override_via_attrlist(self.attrlist.as_ref(), None)
622 }
623 }
624}
625
626impl<'src> HasSpan<'src> for SimpleBlock<'src> {
627 fn span(&self) -> Span<'src> {
628 self.source
629 }
630}
631
632pub(crate) fn is_section_header(line: &str, level_offset: i32) -> bool {
645 let rest = if line.starts_with('=') {
647 line.trim_start_matches('=')
648 } else if line.starts_with('#') {
649 line.trim_start_matches('#')
650 } else {
651 return false;
652 };
653
654 let count = line.len() - rest.len();
658 if count == 0 || count > 6 || !rest.starts_with([' ', '\t']) {
659 return false;
660 }
661
662 let syntactic_level = (count - 1) as i32;
668 syntactic_level > 0 || syntactic_level.saturating_add(level_offset) >= 1
669}
670
671#[cfg(test)]
672mod tests {
673 #![allow(clippy::unwrap_used)]
674
675 use std::ops::Deref;
676
677 use crate::{
678 blocks::{ContentModel, SimpleBlockStyle, metadata::BlockMetadata},
679 tests::prelude::*,
680 };
681
682 #[test]
683 fn impl_clone() {
684 let mut parser = Parser::default();
686
687 let b1 =
688 crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
689
690 let b2 = b1.item.clone();
691 assert_eq!(b1.item, b2);
692 }
693
694 #[test]
695 fn style_enum_impl_debug() {
696 assert_eq!(
697 format!("{:?}", SimpleBlockStyle::Paragraph),
698 "SimpleBlockStyle::Paragraph"
699 );
700
701 assert_eq!(
702 format!("{:?}", SimpleBlockStyle::Literal),
703 "SimpleBlockStyle::Literal"
704 );
705
706 assert_eq!(
707 format!("{:?}", SimpleBlockStyle::Listing),
708 "SimpleBlockStyle::Listing"
709 );
710
711 assert_eq!(
712 format!("{:?}", SimpleBlockStyle::Source),
713 "SimpleBlockStyle::Source"
714 );
715 }
716
717 #[test]
718 fn empty_source() {
719 let mut parser = Parser::default();
720 assert!(crate::blocks::SimpleBlock::parse(&BlockMetadata::new(""), &mut parser).is_none());
721 }
722
723 #[test]
724 fn only_spaces() {
725 let mut parser = Parser::default();
726 assert!(
727 crate::blocks::SimpleBlock::parse(&BlockMetadata::new(" "), &mut parser).is_none()
728 );
729 }
730
731 #[test]
732 fn single_line() {
733 let mut parser = Parser::default();
734 let mi =
735 crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
736
737 assert_eq!(
738 mi.item,
739 SimpleBlock {
740 content: Content {
741 original: Span {
742 data: "abc",
743 line: 1,
744 col: 1,
745 offset: 0,
746 },
747 rendered: "abc",
748 },
749 source: Span {
750 data: "abc",
751 line: 1,
752 col: 1,
753 offset: 0,
754 },
755 style: SimpleBlockStyle::Paragraph,
756 title_source: None,
757 title: None,
758 caption: None,
759 number: None,
760 anchor: None,
761 anchor_reftext: None,
762 attrlist: None,
763 },
764 );
765
766 assert_eq!(mi.item.content_model(), ContentModel::Simple);
767 assert_eq!(mi.item.rendered_content().unwrap(), "abc");
768 assert_eq!(mi.item.raw_context().deref(), "paragraph");
769 assert_eq!(mi.item.resolved_context().deref(), "paragraph");
770 assert!(mi.item.declared_style().is_none());
771 assert!(mi.item.id().is_none());
772 assert!(mi.item.roles().is_empty());
773 assert!(mi.item.options().is_empty());
774 assert!(mi.item.title_source().is_none());
775 assert!(mi.item.title().is_none());
776 assert!(mi.item.anchor().is_none());
777 assert!(mi.item.anchor_reftext().is_none());
778 assert!(mi.item.attrlist().is_none());
779 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
780
781 assert_eq!(
782 mi.after,
783 Span {
784 data: "",
785 line: 1,
786 col: 4,
787 offset: 3
788 }
789 );
790 }
791
792 #[test]
793 fn multiple_lines() {
794 let mut parser = Parser::default();
795 let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\ndef"), &mut parser)
796 .unwrap();
797
798 assert_eq!(
799 mi.item,
800 SimpleBlock {
801 content: Content {
802 original: Span {
803 data: "abc\ndef",
804 line: 1,
805 col: 1,
806 offset: 0,
807 },
808 rendered: "abc\ndef",
809 },
810 source: Span {
811 data: "abc\ndef",
812 line: 1,
813 col: 1,
814 offset: 0,
815 },
816 style: SimpleBlockStyle::Paragraph,
817 title_source: None,
818 title: None,
819 caption: None,
820 number: None,
821 anchor: None,
822 anchor_reftext: None,
823 attrlist: None,
824 }
825 );
826
827 assert_eq!(
828 mi.after,
829 Span {
830 data: "",
831 line: 2,
832 col: 4,
833 offset: 7
834 }
835 );
836
837 assert_eq!(mi.item.rendered_content().unwrap(), "abc\ndef");
838 }
839
840 #[test]
841 fn consumes_blank_lines_after() {
842 let mut parser = Parser::default();
843 let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\n\ndef"), &mut parser)
844 .unwrap();
845
846 assert_eq!(
847 mi.item,
848 SimpleBlock {
849 content: Content {
850 original: Span {
851 data: "abc",
852 line: 1,
853 col: 1,
854 offset: 0,
855 },
856 rendered: "abc",
857 },
858 source: Span {
859 data: "abc",
860 line: 1,
861 col: 1,
862 offset: 0,
863 },
864 style: SimpleBlockStyle::Paragraph,
865 title_source: None,
866 title: None,
867 caption: None,
868 number: None,
869 anchor: None,
870 anchor_reftext: None,
871 attrlist: None,
872 }
873 );
874
875 assert_eq!(
876 mi.after,
877 Span {
878 data: "def",
879 line: 3,
880 col: 1,
881 offset: 5
882 }
883 );
884 }
885
886 #[test]
887 fn overrides_sub_group_via_subs_attribute() {
888 let mut parser = Parser::default();
889 let mi = crate::blocks::SimpleBlock::parse(
890 &BlockMetadata::new("[subs=quotes]\na<b>c *bold*\n\ndef"),
891 &mut parser,
892 )
893 .unwrap();
894
895 assert_eq!(
896 mi.item,
897 SimpleBlock {
898 content: Content {
899 original: Span {
900 data: "a<b>c *bold*",
901 line: 2,
902 col: 1,
903 offset: 14,
904 },
905 rendered: "a<b>c <strong>bold</strong>",
906 },
907 source: Span {
908 data: "[subs=quotes]\na<b>c *bold*",
909 line: 1,
910 col: 1,
911 offset: 0,
912 },
913 style: SimpleBlockStyle::Paragraph,
914 title_source: None,
915 title: None,
916 caption: None,
917 number: None,
918 anchor: None,
919 anchor_reftext: None,
920 attrlist: Some(Attrlist {
921 attributes: &[ElementAttribute {
922 name: Some("subs"),
923 value: "quotes",
924 shorthand_items: &[],
925 },],
926 anchor: None,
927 source: Span {
928 data: "subs=quotes",
929 line: 1,
930 col: 2,
931 offset: 1,
932 },
933 },),
934 }
935 );
936
937 assert_eq!(
938 mi.after,
939 Span {
940 data: "def",
941 line: 4,
942 col: 1,
943 offset: 28
944 }
945 );
946
947 assert_eq!(
948 mi.item.rendered_content().unwrap(),
949 "a<b>c <strong>bold</strong>"
950 );
951 }
952
953 mod is_section_header {
954 use super::super::is_section_header;
955
956 #[test]
957 fn multi_marker_is_always_a_section_regardless_of_offset() {
958 assert!(is_section_header("== Section", 0));
962 assert!(is_section_header("=== Section", 0));
963 assert!(is_section_header("## Section", 0));
964 assert!(is_section_header("== Section", -1));
965 }
966
967 #[test]
968 fn single_marker_is_a_section_only_under_positive_offset() {
969 assert!(!is_section_header("= Title", 0));
972 assert!(!is_section_header("# Title", 0));
973 assert!(is_section_header("= Title", 1));
974 assert!(is_section_header("# Title", 1));
975 assert!(is_section_header("= Title", 2));
976 }
977
978 #[test]
979 fn requires_a_blank_after_the_marker() {
980 assert!(!is_section_header("==nospace", 0));
981 assert!(!is_section_header("=nospace", 1));
982 assert!(!is_section_header("##nospace", 0));
983 }
984
985 #[test]
986 fn accepts_a_tab_after_the_marker() {
987 assert!(is_section_header("==\tSection", 0));
991 assert!(is_section_header("=\tSection", 1));
992 assert!(!is_section_header("=\tSection", 0));
993 }
994
995 #[test]
996 fn non_marker_and_over_long_marker_are_not_sections() {
997 assert!(!is_section_header("paragraph", 1));
998
999 assert!(!is_section_header("======= Too deep", 0));
1001 }
1002 }
1003}