mdbook_lint_core/rules/standard/
md058.rs1use crate::error::Result;
6use crate::rule::{AstRule, RuleCategory, RuleMetadata};
7use crate::{
8 Document,
9 violation::{Severity, Violation},
10};
11use comrak::nodes::{AstNode, NodeValue};
12
13pub struct MD058;
15
16impl MD058 {
17 fn get_position<'a>(&self, node: &'a AstNode<'a>) -> (usize, usize) {
19 let data = node.data.borrow();
20 let pos = data.sourcepos;
21 (pos.start.line, pos.start.column)
22 }
23
24 fn is_blank_line(&self, line: &str) -> bool {
26 line.trim().is_empty()
27 }
28
29 fn check_node<'a>(
31 &self,
32 node: &'a AstNode<'a>,
33 violations: &mut Vec<Violation>,
34 document: &Document,
35 ) {
36 if let NodeValue::Table(_) = &node.data.borrow().value {
37 let (start_line, _) = self.get_position(node);
38 let lines: Vec<&str> = document.content.lines().collect();
39
40 let table_segments = self.find_table_segments(start_line, &lines);
42
43 for (segment_start, segment_end) in table_segments {
44 if segment_start > 1 {
46 let line_before_idx = segment_start - 2; if line_before_idx < lines.len() && !self.is_blank_line(lines[line_before_idx])
48 {
49 violations.push(self.create_violation(
50 "Tables should be preceded by a blank line".to_string(),
51 segment_start,
52 1,
53 Severity::Warning,
54 ));
55 }
56 }
57
58 if segment_end < lines.len() {
60 let line_after_idx = segment_end; if line_after_idx < lines.len() {
62 let line_after = lines[line_after_idx];
63 if !self.is_blank_line(line_after) {
64 violations.push(self.create_violation(
65 "Tables should be followed by a blank line".to_string(),
66 segment_end + 1, 1,
68 Severity::Warning,
69 ));
70 }
71 }
72 }
73 }
74 }
75
76 for child in node.children() {
79 self.check_node(child, violations, document);
80 }
81 }
82
83 fn find_table_segments(&self, start_line: usize, lines: &[&str]) -> Vec<(usize, usize)> {
85 let mut segments = Vec::new();
86 let mut current_line = start_line - 1; while current_line < lines.len() {
89 let line = lines[current_line].trim();
90
91 if !line.contains('|') {
93 current_line += 1;
94 continue;
95 }
96
97 let segment_start = current_line + 1; while current_line < lines.len() {
102 let line = lines[current_line].trim();
103
104 if line.contains('|') {
105 if line
107 .chars()
108 .all(|c| c == '|' || c == '-' || c == ':' || c.is_whitespace())
109 {
110 current_line += 1;
111 continue;
112 }
113
114 let pipe_count = line.chars().filter(|&c| c == '|').count();
116 if pipe_count >= 1 {
117 current_line += 1;
118 continue;
119 }
120 }
121
122 break;
124 }
125
126 let segment_end = current_line; segments.push((segment_start, segment_end));
128
129 while current_line < lines.len() {
131 let line = lines[current_line].trim();
132 if line.contains('|') {
133 break; }
135 if line.is_empty() {
136 break; }
138 current_line += 1;
139 }
140 }
141
142 segments
143 }
144}
145
146impl AstRule for MD058 {
147 fn id(&self) -> &'static str {
148 "MD058"
149 }
150
151 fn name(&self) -> &'static str {
152 "blanks-around-tables"
153 }
154
155 fn description(&self) -> &'static str {
156 "Tables should be surrounded by blank lines"
157 }
158
159 fn metadata(&self) -> RuleMetadata {
160 RuleMetadata::stable(RuleCategory::Formatting).introduced_in("mdbook-lint v0.1.0")
161 }
162
163 fn check_ast<'a>(&self, document: &Document, ast: &'a AstNode<'a>) -> Result<Vec<Violation>> {
164 let mut violations = Vec::new();
165 self.check_node(ast, &mut violations, document);
166 Ok(violations)
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::rule::Rule;
174 use std::path::PathBuf;
175
176 fn create_test_document(content: &str) -> Document {
177 Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
178 }
179
180 #[test]
181 fn test_md058_tables_with_blank_lines_valid() {
182 let content = r#"Here is some text.
183
184| Column 1 | Column 2 |
185|----------|----------|
186| Value 1 | Value 2 |
187
188More text after the table.
189"#;
190
191 let document = create_test_document(content);
192 let rule = MD058;
193 let violations = rule.check(&document).unwrap();
194 assert_eq!(violations.len(), 0);
195 }
196
197 #[test]
198 fn test_md058_table_at_start_of_document() {
199 let content = r#"| Column 1 | Column 2 |
200|----------|----------|
201| Value 1 | Value 2 |
202
203Text after the table.
204"#;
205
206 let document = create_test_document(content);
207 let rule = MD058;
208 let violations = rule.check(&document).unwrap();
209 assert_eq!(violations.len(), 0);
210 }
211
212 #[test]
213 fn test_md058_table_at_end_of_document() {
214 let content = r#"Some text before.
215
216| Column 1 | Column 2 |
217|----------|----------|
218| Value 1 | Value 2 |"#;
219
220 let document = create_test_document(content);
221 let rule = MD058;
222 let violations = rule.check(&document).unwrap();
223 assert_eq!(violations.len(), 0);
224 }
225
226 #[test]
227 fn test_md058_table_missing_blank_before() {
228 let content = r#"Here is some text.
229| Column 1 | Column 2 |
230|----------|----------|
231| Value 1 | Value 2 |
232
233More text after.
234"#;
235
236 let document = create_test_document(content);
237 let rule = MD058;
238 let violations = rule.check(&document).unwrap();
239 assert_eq!(violations.len(), 1);
240 assert_eq!(violations[0].rule_id, "MD058");
241 assert!(violations[0].message.contains("preceded by a blank line"));
242 assert_eq!(violations[0].line, 2);
243 }
244
245 #[test]
246 fn test_md058_table_missing_blank_after() {
247 let content = r#"Some text before.
248
249| Column 1 | Column 2 |
250|----------|----------|
251| Value 1 | Value 2 |
252More text after.
253"#;
254
255 let document = create_test_document(content);
256 let rule = MD058;
257 let violations = rule.check(&document).unwrap();
258
259 assert_eq!(violations.len(), 1);
260 assert!(violations[0].message.contains("followed by a blank line"));
261 assert_eq!(violations[0].line, 6);
262 }
263
264 #[test]
265 fn test_md058_table_missing_both_blanks() {
266 let content = r#"Text before.
267| Column 1 | Column 2 |
268|----------|----------|
269| Value 1 | Value 2 |
270Text after.
271"#;
272
273 let document = create_test_document(content);
274 let rule = MD058;
275 let violations = rule.check(&document).unwrap();
276 assert_eq!(violations.len(), 2);
277 assert!(violations[0].message.contains("preceded by a blank line"));
278 assert!(violations[1].message.contains("followed by a blank line"));
279 }
280
281 #[test]
282 fn test_md058_multiple_tables() {
283 let content = r#"First table with proper spacing:
284
285| Table 1 | Column 2 |
286|----------|----------|
287| Value 1 | Value 2 |
288
289Second table also with proper spacing:
290
291| Table 2 | Column 2 |
292|----------|----------|
293| Value 3 | Value 4 |
294
295End of document.
296"#;
297
298 let document = create_test_document(content);
299 let rule = MD058;
300 let violations = rule.check(&document).unwrap();
301 assert_eq!(violations.len(), 0);
302 }
303
304 #[test]
305 fn test_md058_multiple_tables_violations() {
306 let content = r#"First table:
307| Table 1 | Column 2 |
308|----------|----------|
309| Value 1 | Value 2 |
310Second table immediately after:
311| Table 2 | Column 2 |
312|----------|----------|
313| Value 3 | Value 4 |
314End text.
315"#;
316
317 let document = create_test_document(content);
318 let rule = MD058;
319 let violations = rule.check(&document).unwrap();
320 assert_eq!(violations.len(), 4); }
322
323 #[test]
324 fn test_md058_table_only_document() {
325 let content = r#"| Column 1 | Column 2 |
326|----------|----------|
327| Value 1 | Value 2 |"#;
328
329 let document = create_test_document(content);
330 let rule = MD058;
331 let violations = rule.check(&document).unwrap();
332 assert_eq!(violations.len(), 0); }
334
335 #[test]
336 fn test_md058_tables_with_different_content() {
337 let content = r#"# Heading before table
338| Column 1 | Column 2 |
339|----------|----------|
340| Value 1 | Value 2 |
341
342## Heading after table
343
344Some paragraph.
345
346| Another | Table |
347|---------|-------|
348| More | Data |
349
350- List item after table
351"#;
352
353 let document = create_test_document(content);
354 let rule = MD058;
355 let violations = rule.check(&document).unwrap();
356 assert_eq!(violations.len(), 1); assert!(violations[0].message.contains("preceded by a blank line"));
358 }
359
360 #[test]
361 fn test_md058_complex_table() {
362 let content = r#"Text before.
363
364| Left | Center | Right | Numbers |
365|:-----|:------:|------:|--------:|
366| L1 | C1 | R1 | 123 |
367| L2 | C2 | R2 | 456 |
368| L3 | C3 | R3 | 789 |
369
370Text after.
371"#;
372
373 let document = create_test_document(content);
374 let rule = MD058;
375 let violations = rule.check(&document).unwrap();
376 assert_eq!(violations.len(), 0);
377 }
378
379 #[test]
380 fn test_md058_table_with_empty_cells() {
381 let content = r#"Before text.
382
383| Col1 | Col2 | Col3 |
384|------|------|------|
385| A | | C |
386| | B | |
387| X | Y | Z |
388
389After text.
390"#;
391
392 let document = create_test_document(content);
393 let rule = MD058;
394 let violations = rule.check(&document).unwrap();
395 assert_eq!(violations.len(), 0);
396 }
397}