mdbook_lint_core/rules/standard/
md028.rs1use crate::error::Result;
7use crate::rule::{Rule, RuleCategory, RuleMetadata};
8use crate::{
9 Document,
10 violation::{Severity, Violation},
11};
12
13pub struct MD028;
15
16impl Rule for MD028 {
17 fn id(&self) -> &'static str {
18 "MD028"
19 }
20
21 fn name(&self) -> &'static str {
22 "no-blanks-blockquote"
23 }
24
25 fn description(&self) -> &'static str {
26 "Blank line inside blockquote"
27 }
28
29 fn metadata(&self) -> RuleMetadata {
30 RuleMetadata::stable(RuleCategory::Formatting).introduced_in("mdbook-lint v0.1.0")
31 }
32
33 fn check_with_ast<'a>(
34 &self,
35 document: &Document,
36 _ast: Option<&'a comrak::nodes::AstNode<'a>>,
37 ) -> Result<Vec<Violation>> {
38 let mut violations = Vec::new();
39
40 for (line_number, line) in document.lines.iter().enumerate() {
41 let line_num = line_number + 1; if line.trim().is_empty() {
45 let mut prev_is_blockquote = false;
47 for i in (0..line_num - 1).rev() {
48 if let Some(prev_line) = document.lines.get(i)
49 && !prev_line.trim().is_empty()
50 {
51 prev_is_blockquote = prev_line.trim_start().starts_with('>');
52 break;
53 }
54 }
55
56 let mut next_is_blockquote = false;
58 for i in line_num..document.lines.len() {
59 if let Some(next_line) = document.lines.get(i)
60 && !next_line.trim().is_empty()
61 {
62 next_is_blockquote = next_line.trim_start().starts_with('>');
63 break;
64 }
65 }
66
67 if prev_is_blockquote && next_is_blockquote {
70 violations.push(self.create_violation(
71 "Blank line inside blockquote".to_string(),
72 line_num,
73 1,
74 Severity::Warning,
75 ));
76 }
77 }
78 }
79
80 Ok(violations)
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::Document;
88 use crate::rule::Rule;
89 use std::path::PathBuf;
90
91 #[test]
92 fn test_md028_no_violations() {
93 let content = r#"> This is a valid blockquote
94> with multiple lines
95> all properly formatted
96
97Regular paragraph here.
98
99> Another blockquote
100> also properly formatted
101>
102> with empty blockquote line
103
104More regular text.
105
106> Single line blockquote
107
108Final paragraph.
109"#;
110 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
111 let rule = MD028;
112 let violations = rule.check(&document).unwrap();
113
114 assert_eq!(violations.len(), 0);
115 }
116
117 #[test]
118 fn test_md028_blank_line_violation() {
119 let content = r#"> This is a blockquote
120> with proper formatting
121
122> but then it continues
123> after a blank line
124
125Regular text here.
126"#;
127 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
128 let rule = MD028;
129 let violations = rule.check(&document).unwrap();
130
131 assert_eq!(violations.len(), 1);
132 assert_eq!(violations[0].line, 3); assert!(
134 violations[0]
135 .message
136 .contains("Blank line inside blockquote")
137 );
138 }
139
140 #[test]
141 fn test_md028_multiple_blank_lines() {
142 let content = r#"> Start of blockquote
143> with some content
144
145> continues after blank line
146
147
148> continues after multiple blank lines
149> and ends here
150
151Regular text.
152"#;
153 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
154 let rule = MD028;
155 let violations = rule.check(&document).unwrap();
156
157 assert_eq!(violations.len(), 3);
158 assert_eq!(violations[0].line, 3); assert_eq!(violations[1].line, 5); assert_eq!(violations[2].line, 6); }
162
163 #[test]
164 fn test_md028_proper_blockquote_separation() {
165 let content = r#"> First blockquote
166> ends here
167
168Regular paragraph in between.
169
170> Second blockquote
171> starts here
172
173More regular text.
174"#;
175 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
176 let rule = MD028;
177 let violations = rule.check(&document).unwrap();
178
179 assert_eq!(violations.len(), 0);
181 }
182
183 #[test]
184 fn test_md028_nested_blockquotes() {
185 let content = r#"> Outer blockquote
186> > Inner blockquote
187> > continues here
188
189> > but this breaks the flow
190> back to outer level
191
192Text here.
193"#;
194 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
195 let rule = MD028;
196 let violations = rule.check(&document).unwrap();
197
198 assert_eq!(violations.len(), 1);
199 assert_eq!(violations[0].line, 4); }
201
202 #[test]
203 fn test_md028_blockquote_at_end() {
204 let content = r#"> Blockquote at the end
205> of the document
206
207> continues here"#;
208 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
209 let rule = MD028;
210 let violations = rule.check(&document).unwrap();
211
212 assert_eq!(violations.len(), 1);
213 assert_eq!(violations[0].line, 3);
214 }
215
216 #[test]
217 fn test_md028_empty_blockquote_lines() {
218 let content = r#"> Blockquote with empty lines
219>
220> is perfectly valid
221>
222> because empty lines have >
223
224Regular text.
225"#;
226 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
227 let rule = MD028;
228 let violations = rule.check(&document).unwrap();
229
230 assert_eq!(violations.len(), 0);
232 }
233
234 #[test]
235 fn test_md028_indented_blockquotes() {
236 let content = r#"Regular text.
237
238 > Indented blockquote
239 > continues here
240
241 > but breaks here
242 > and continues
243
244More text.
245"#;
246 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
247 let rule = MD028;
248 let violations = rule.check(&document).unwrap();
249
250 assert_eq!(violations.len(), 1);
251 assert_eq!(violations[0].line, 5);
252 }
253
254 #[test]
255 fn test_md028_complex_document() {
256 let content = r#"# Heading
257
258> Valid blockquote
259> with multiple lines
260
261Regular paragraph.
262
263> Another blockquote
264
265> that continues improperly
266
267> and has more content
268
269## Another heading
270
271> Final blockquote
272> that ends properly
273
274The end.
275"#;
276 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
277 let rule = MD028;
278 let violations = rule.check(&document).unwrap();
279
280 assert_eq!(violations.len(), 2);
281 assert_eq!(violations[0].line, 9); assert_eq!(violations[1].line, 11); }
284}