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 mut next = source;
330 let mut filtered_lines: Vec<&'src str> = vec![];
331 let mut skipped_comment_line = false;
332
333 let in_definition_list = parent_list_markers
338 .iter()
339 .any(|m| matches!(m, ListItemMarker::DefinedTerm { .. }));
340
341 let strip_indent =
342 if preserve_literal_indent && style == SimpleBlockStyle::Literal && in_definition_list {
343 let mut scan = source;
345 let mut min_indent = first_line_indent;
346 let mut line_count = 0;
347
348 while let Some(line_mi) = scan.take_non_empty_line() {
349 let line = line_mi.item;
350
351 if line_count > 0 && line.data() == "+" {
353 break;
354 }
355
356 if let Some(n) = line.position(|c| c != ' ' && c != '\t') {
357 min_indent = min_indent.min(n);
358 }
359
360 line_count += 1;
361 scan = line_mi.after;
362 }
363 min_indent
364 } else {
365 first_line_indent
366 };
367
368 while let Some(line_mi) = next.take_non_empty_line() {
369 let mut line = line_mi.item;
370
371 if !stop_for_list_items
375 && skipped_comment_line
376 && style == SimpleBlockStyle::Paragraph
377 && is_section_header(line.data())
378 {
379 break;
380 }
381
382 if !filtered_lines.is_empty() {
387 let should_check_for_list_marker =
390 stop_for_list_items && (!indented_literal_mode || line.col() == 1);
391
392 if should_check_for_list_marker
396 && let Some(marker_mi) = ListItemMarker::parse(line, parser)
397 {
398 let is_ancestor_list = parent_list_markers
402 .iter()
403 .any(|p| p.is_match_for(&marker_mi.item));
404
405 if is_ancestor_list || !preserve_literal_indent {
406 break;
407 }
408 }
409
410 if line.data() == "+" {
411 break;
412 }
413
414 if line.starts_with('[') && line.ends_with(']') {
415 break;
416 }
417
418 if (line.starts_with('/')
419 || line.starts_with('-')
420 || line.starts_with('.')
421 || line.starts_with('+')
422 || line.starts_with('=')
423 || line.starts_with('*')
424 || line.starts_with('_'))
425 && (RawDelimitedBlock::is_valid_delimiter(&line)
426 || CompoundDelimitedBlock::is_valid_delimiter(&line))
427 {
428 break;
429 }
430 }
431
432 next = line_mi.after;
433
434 if style == SimpleBlockStyle::Paragraph
437 && line.starts_with("//")
438 && !line.starts_with("///")
439 {
440 skipped_comment_line = true;
441 continue;
442 }
443
444 let should_strip_indent = strip_indent > 0;
446
447 if should_strip_indent && let Some(n) = line.position(|c| c != ' ' && c != '\t') {
448 line = line.into_parse_result(n.min(strip_indent)).after;
449 };
450
451 filtered_lines.push(line.trim_trailing_whitespace().data());
452 }
453
454 let source = source.trim_remainder(next).trim_trailing_whitespace();
455 if source.is_empty() {
456 return None;
457 }
458
459 let filtered_lines = filtered_lines.join("\n");
460 let mut content: Content<'src> = Content::from_filtered(source, filtered_lines);
461
462 let sub_group = match style {
463 SimpleBlockStyle::Literal => SubstitutionGroup::Verbatim,
466 SimpleBlockStyle::Listing | SimpleBlockStyle::Source | SimpleBlockStyle::Paragraph => {
467 SubstitutionGroup::Normal
468 }
469 };
470
471 sub_group.override_via_attrlist(attrlist.as_ref()).apply(
472 &mut content,
473 parser,
474 attrlist.as_ref(),
475 );
476
477 Some(MatchedItem {
478 item: (content, style),
479 after: next,
480 })
481}
482
483impl<'src> IsBlock<'src> for SimpleBlock<'src> {
484 fn content_model(&self) -> ContentModel {
485 ContentModel::Simple
486 }
487
488 fn content_mut(&mut self) -> Option<&mut Content<'src>> {
489 Some(&mut self.content)
490 }
491
492 fn rendered_content(&self) -> Option<&str> {
493 Some(self.content.rendered())
494 }
495
496 fn raw_context(&self) -> CowStr<'src> {
497 "paragraph".into()
498 }
499
500 fn title_source(&'src self) -> Option<Span<'src>> {
501 self.title_source
502 }
503
504 fn title(&self) -> Option<&str> {
505 self.title.as_deref()
506 }
507
508 fn caption(&self) -> Option<&str> {
509 self.caption.as_deref()
510 }
511
512 fn number(&self) -> Option<usize> {
513 self.number
514 }
515
516 fn anchor(&'src self) -> Option<Span<'src>> {
517 self.anchor
518 }
519
520 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
521 self.anchor_reftext
522 }
523
524 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
525 self.attrlist.as_ref()
526 }
527}
528
529impl<'src> HasSpan<'src> for SimpleBlock<'src> {
530 fn span(&self) -> Span<'src> {
531 self.source
532 }
533}
534
535fn is_section_header(line: &str) -> bool {
538 if line.starts_with("==") {
540 let rest = line.trim_start_matches('=');
541 if rest.starts_with(' ') {
542 return true;
543 }
544 }
545
546 if line.starts_with("##") {
548 let rest = line.trim_start_matches('#');
549 if rest.starts_with(' ') {
550 return true;
551 }
552 }
553
554 false
555}
556
557#[cfg(test)]
558mod tests {
559 #![allow(clippy::unwrap_used)]
560
561 use std::ops::Deref;
562
563 use crate::{
564 blocks::{ContentModel, SimpleBlockStyle, metadata::BlockMetadata},
565 tests::prelude::*,
566 };
567
568 #[test]
569 fn impl_clone() {
570 let mut parser = Parser::default();
572
573 let b1 =
574 crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
575
576 let b2 = b1.item.clone();
577 assert_eq!(b1.item, b2);
578 }
579
580 #[test]
581 fn style_enum_impl_debug() {
582 assert_eq!(
583 format!("{:?}", SimpleBlockStyle::Paragraph),
584 "SimpleBlockStyle::Paragraph"
585 );
586
587 assert_eq!(
588 format!("{:?}", SimpleBlockStyle::Literal),
589 "SimpleBlockStyle::Literal"
590 );
591
592 assert_eq!(
593 format!("{:?}", SimpleBlockStyle::Listing),
594 "SimpleBlockStyle::Listing"
595 );
596
597 assert_eq!(
598 format!("{:?}", SimpleBlockStyle::Source),
599 "SimpleBlockStyle::Source"
600 );
601 }
602
603 #[test]
604 fn empty_source() {
605 let mut parser = Parser::default();
606 assert!(crate::blocks::SimpleBlock::parse(&BlockMetadata::new(""), &mut parser).is_none());
607 }
608
609 #[test]
610 fn only_spaces() {
611 let mut parser = Parser::default();
612 assert!(
613 crate::blocks::SimpleBlock::parse(&BlockMetadata::new(" "), &mut parser).is_none()
614 );
615 }
616
617 #[test]
618 fn single_line() {
619 let mut parser = Parser::default();
620 let mi =
621 crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
622
623 assert_eq!(
624 mi.item,
625 SimpleBlock {
626 content: Content {
627 original: Span {
628 data: "abc",
629 line: 1,
630 col: 1,
631 offset: 0,
632 },
633 rendered: "abc",
634 },
635 source: Span {
636 data: "abc",
637 line: 1,
638 col: 1,
639 offset: 0,
640 },
641 style: SimpleBlockStyle::Paragraph,
642 title_source: None,
643 title: None,
644 caption: None,
645 number: None,
646 anchor: None,
647 anchor_reftext: None,
648 attrlist: None,
649 },
650 );
651
652 assert_eq!(mi.item.content_model(), ContentModel::Simple);
653 assert_eq!(mi.item.rendered_content().unwrap(), "abc");
654 assert_eq!(mi.item.raw_context().deref(), "paragraph");
655 assert_eq!(mi.item.resolved_context().deref(), "paragraph");
656 assert!(mi.item.declared_style().is_none());
657 assert!(mi.item.id().is_none());
658 assert!(mi.item.roles().is_empty());
659 assert!(mi.item.options().is_empty());
660 assert!(mi.item.title_source().is_none());
661 assert!(mi.item.title().is_none());
662 assert!(mi.item.anchor().is_none());
663 assert!(mi.item.anchor_reftext().is_none());
664 assert!(mi.item.attrlist().is_none());
665 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
666
667 assert_eq!(
668 mi.after,
669 Span {
670 data: "",
671 line: 1,
672 col: 4,
673 offset: 3
674 }
675 );
676 }
677
678 #[test]
679 fn multiple_lines() {
680 let mut parser = Parser::default();
681 let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\ndef"), &mut parser)
682 .unwrap();
683
684 assert_eq!(
685 mi.item,
686 SimpleBlock {
687 content: Content {
688 original: Span {
689 data: "abc\ndef",
690 line: 1,
691 col: 1,
692 offset: 0,
693 },
694 rendered: "abc\ndef",
695 },
696 source: Span {
697 data: "abc\ndef",
698 line: 1,
699 col: 1,
700 offset: 0,
701 },
702 style: SimpleBlockStyle::Paragraph,
703 title_source: None,
704 title: None,
705 caption: None,
706 number: None,
707 anchor: None,
708 anchor_reftext: None,
709 attrlist: None,
710 }
711 );
712
713 assert_eq!(
714 mi.after,
715 Span {
716 data: "",
717 line: 2,
718 col: 4,
719 offset: 7
720 }
721 );
722
723 assert_eq!(mi.item.rendered_content().unwrap(), "abc\ndef");
724 }
725
726 #[test]
727 fn consumes_blank_lines_after() {
728 let mut parser = Parser::default();
729 let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\n\ndef"), &mut parser)
730 .unwrap();
731
732 assert_eq!(
733 mi.item,
734 SimpleBlock {
735 content: Content {
736 original: Span {
737 data: "abc",
738 line: 1,
739 col: 1,
740 offset: 0,
741 },
742 rendered: "abc",
743 },
744 source: Span {
745 data: "abc",
746 line: 1,
747 col: 1,
748 offset: 0,
749 },
750 style: SimpleBlockStyle::Paragraph,
751 title_source: None,
752 title: None,
753 caption: None,
754 number: None,
755 anchor: None,
756 anchor_reftext: None,
757 attrlist: None,
758 }
759 );
760
761 assert_eq!(
762 mi.after,
763 Span {
764 data: "def",
765 line: 3,
766 col: 1,
767 offset: 5
768 }
769 );
770 }
771
772 #[test]
773 fn overrides_sub_group_via_subs_attribute() {
774 let mut parser = Parser::default();
775 let mi = crate::blocks::SimpleBlock::parse(
776 &BlockMetadata::new("[subs=quotes]\na<b>c *bold*\n\ndef"),
777 &mut parser,
778 )
779 .unwrap();
780
781 assert_eq!(
782 mi.item,
783 SimpleBlock {
784 content: Content {
785 original: Span {
786 data: "a<b>c *bold*",
787 line: 2,
788 col: 1,
789 offset: 14,
790 },
791 rendered: "a<b>c <strong>bold</strong>",
792 },
793 source: Span {
794 data: "[subs=quotes]\na<b>c *bold*",
795 line: 1,
796 col: 1,
797 offset: 0,
798 },
799 style: SimpleBlockStyle::Paragraph,
800 title_source: None,
801 title: None,
802 caption: None,
803 number: None,
804 anchor: None,
805 anchor_reftext: None,
806 attrlist: Some(Attrlist {
807 attributes: &[ElementAttribute {
808 name: Some("subs"),
809 value: "quotes",
810 shorthand_items: &[],
811 },],
812 anchor: None,
813 source: Span {
814 data: "subs=quotes",
815 line: 1,
816 col: 2,
817 offset: 1,
818 },
819 },),
820 }
821 );
822
823 assert_eq!(
824 mi.after,
825 Span {
826 data: "def",
827 line: 4,
828 col: 1,
829 offset: 28
830 }
831 );
832
833 assert_eq!(
834 mi.item.rendered_content().unwrap(),
835 "a<b>c <strong>bold</strong>"
836 );
837 }
838}