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