marco_core/parser/blocks/
cm_blockquote_parser.rs1use super::shared::{opt_span, GrammarSpan};
7use crate::parser::ast::{Document, Node, NodeKind};
8
9pub fn parse_blockquote<F>(
33 content: GrammarSpan,
34 depth: usize,
35 parse_blocks_fn: F,
36) -> Result<Node, Box<dyn std::error::Error>>
37where
38 F: FnOnce(&str, usize) -> Result<Document, Box<dyn std::error::Error>>,
39{
40 let span = opt_span(content);
41
42 let content_str = content.fragment();
46 let mut cleaned_content = String::with_capacity(content_str.len());
47
48 for line in content_str.split_inclusive('\n') {
49 let line_trimmed_start = line.trim_start();
50 let has_marker = line_trimmed_start.starts_with('>');
51
52 if has_marker {
53 let after_marker = line_trimmed_start.strip_prefix('>').unwrap();
55 let cleaned = after_marker.strip_prefix(' ').unwrap_or(after_marker);
56 cleaned_content.push_str(cleaned);
57 } else {
58 let line_content = line_trimmed_start.trim_end();
61 let line_sans_spaces = line_content.replace([' ', '\t'], "");
62
63 let is_underline = !line_sans_spaces.is_empty()
64 && (line_sans_spaces.chars().all(|c| c == '=')
65 || line_sans_spaces.chars().all(|c| c == '-'));
66
67 if is_underline {
68 if let Some(first_char) = line_content.chars().next() {
72 if first_char == '=' || first_char == '-' {
73 cleaned_content.push('\\');
75 }
76 }
77 }
78
79 cleaned_content.push_str(line);
81 }
82 }
83
84 let inner_doc = parse_blocks_fn(&cleaned_content, depth + 1)?;
86
87 Ok(Node {
88 kind: NodeKind::Blockquote,
89 span,
90 children: inner_doc.children, })
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::parser::ast::NodeKind;
98
99 fn mock_parse_blocks(
101 input: &str,
102 _depth: usize,
103 ) -> Result<Document, Box<dyn std::error::Error>> {
104 let mut doc = Document::new();
105 if !input.is_empty() {
106 doc.children.push(Node {
107 kind: NodeKind::Text(input.to_string()),
108 span: None,
109 children: Vec::new(),
110 });
111 }
112 Ok(doc)
113 }
114
115 #[test]
116 fn smoke_test_parse_blockquote_basic() {
117 let content = GrammarSpan::new("> Line 1\n> Line 2");
118 let node = parse_blockquote(content, 0, mock_parse_blocks).unwrap();
119
120 assert!(matches!(node.kind, NodeKind::Blockquote));
121 assert!(!node.children.is_empty());
122 }
123
124 #[test]
125 fn smoke_test_blockquote_lazy_continuation() {
126 let content = GrammarSpan::new("> Line 1\nLine 2 (lazy)");
127 let node = parse_blockquote(content, 0, mock_parse_blocks).unwrap();
128
129 assert!(matches!(node.kind, NodeKind::Blockquote));
130 }
131
132 #[test]
133 fn smoke_test_blockquote_span() {
134 let content = GrammarSpan::new("> Test");
135 let node = parse_blockquote(content, 0, mock_parse_blocks).unwrap();
136
137 assert!(node.span.is_some());
138 }
139
140 #[test]
141 fn smoke_test_blockquote_empty() {
142 let content = GrammarSpan::new(">");
143 let node = parse_blockquote(content, 0, mock_parse_blocks).unwrap();
144
145 assert!(matches!(node.kind, NodeKind::Blockquote));
146 }
147
148 #[test]
149 fn smoke_test_blockquote_nested_content() {
150 let content = GrammarSpan::new("> # Heading\n> Paragraph");
151 let node = parse_blockquote(content, 0, mock_parse_blocks).unwrap();
152
153 assert!(matches!(node.kind, NodeKind::Blockquote));
154 assert!(!node.children.is_empty());
155 }
156}