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