rumdl_lib/rules/
md058_blanks_around_tables.rs

1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, Severity};
2use crate::rule_config_serde::RuleConfig;
3use crate::utils::kramdown_utils::is_kramdown_block_attribute;
4use serde::{Deserialize, Serialize};
5
6/// Rule MD058: Blanks around tables
7///
8/// See [docs/md058.md](../../docs/md058.md) for full documentation, configuration, and examples.
9///
10/// Ensures tables have blank lines before and after them
11///
12/// Configuration for MD058 rule
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[serde(rename_all = "kebab-case")]
15pub struct MD058Config {
16    /// Minimum number of blank lines before tables
17    #[serde(default = "default_minimum_before")]
18    pub minimum_before: usize,
19    /// Minimum number of blank lines after tables
20    #[serde(default = "default_minimum_after")]
21    pub minimum_after: usize,
22}
23
24impl Default for MD058Config {
25    fn default() -> Self {
26        Self {
27            minimum_before: default_minimum_before(),
28            minimum_after: default_minimum_after(),
29        }
30    }
31}
32
33fn default_minimum_before() -> usize {
34    1
35}
36
37fn default_minimum_after() -> usize {
38    1
39}
40
41impl RuleConfig for MD058Config {
42    const RULE_NAME: &'static str = "MD058";
43}
44
45#[derive(Clone, Default)]
46pub struct MD058BlanksAroundTables {
47    config: MD058Config,
48}
49
50impl MD058BlanksAroundTables {
51    /// Create a new instance with the given configuration
52    pub fn from_config_struct(config: MD058Config) -> Self {
53        Self { config }
54    }
55
56    /// Check if a line is blank
57    fn is_blank_line(&self, line: &str) -> bool {
58        line.trim().is_empty()
59    }
60
61    /// Count the number of blank lines before a given line index
62    fn count_blank_lines_before(&self, lines: &[&str], line_index: usize) -> usize {
63        let mut count = 0;
64        let mut i = line_index;
65        while i > 0 {
66            i -= 1;
67            if self.is_blank_line(lines[i]) {
68                count += 1;
69            } else {
70                break;
71            }
72        }
73        count
74    }
75
76    /// Count the number of blank lines after a given line index
77    fn count_blank_lines_after(&self, lines: &[&str], line_index: usize) -> usize {
78        let mut count = 0;
79        let mut i = line_index + 1;
80        while i < lines.len() {
81            if self.is_blank_line(lines[i]) {
82                count += 1;
83                i += 1;
84            } else {
85                break;
86            }
87        }
88        count
89    }
90}
91
92impl Rule for MD058BlanksAroundTables {
93    fn name(&self) -> &'static str {
94        "MD058"
95    }
96
97    fn description(&self) -> &'static str {
98        "Tables should be surrounded by blank lines"
99    }
100
101    fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
102        // Skip if no tables present
103        !ctx.likely_has_tables()
104    }
105
106    fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
107        let content = ctx.content;
108        let _line_index = &ctx.line_index;
109        let mut warnings = Vec::new();
110
111        // Early return for empty content or content without tables
112        if content.is_empty() || !content.contains('|') {
113            return Ok(Vec::new());
114        }
115
116        let lines: Vec<&str> = content.lines().collect();
117
118        // Use pre-computed table blocks from context
119        let table_blocks = &ctx.table_blocks;
120
121        for table_block in table_blocks {
122            // Check for sufficient blank lines before table
123            if table_block.start_line > 0 {
124                let blank_lines_before = self.count_blank_lines_before(&lines, table_block.start_line);
125                if blank_lines_before < self.config.minimum_before {
126                    let needed = self.config.minimum_before - blank_lines_before;
127                    let message = if self.config.minimum_before == 1 {
128                        "Missing blank line before table".to_string()
129                    } else {
130                        format!("Missing {needed} blank lines before table")
131                    };
132
133                    warnings.push(LintWarning {
134                        rule_name: Some(self.name().to_string()),
135                        message,
136                        line: table_block.start_line + 1,
137                        column: 1,
138                        end_line: table_block.start_line + 1,
139                        end_column: 2,
140                        severity: Severity::Warning,
141                        fix: Some(Fix {
142                            // Insert blank lines at the start of the table line
143                            range: _line_index.line_col_to_byte_range(table_block.start_line + 1, 1),
144                            replacement: "\n".repeat(needed),
145                        }),
146                    });
147                }
148            }
149
150            // Check for sufficient blank lines after table
151            if table_block.end_line < lines.len() - 1 {
152                // Check if the next line is a Kramdown block attribute
153                let next_line_is_attribute = if table_block.end_line + 1 < lines.len() {
154                    is_kramdown_block_attribute(lines[table_block.end_line + 1])
155                } else {
156                    false
157                };
158
159                // Skip check if next line is a block attribute
160                if !next_line_is_attribute {
161                    let blank_lines_after = self.count_blank_lines_after(&lines, table_block.end_line);
162                    if blank_lines_after < self.config.minimum_after {
163                        let needed = self.config.minimum_after - blank_lines_after;
164                        let message = if self.config.minimum_after == 1 {
165                            "Missing blank line after table".to_string()
166                        } else {
167                            format!("Missing {needed} blank lines after table")
168                        };
169
170                        warnings.push(LintWarning {
171                            rule_name: Some(self.name().to_string()),
172                            message,
173                            line: table_block.end_line + 1,
174                            column: lines[table_block.end_line].len() + 1,
175                            end_line: table_block.end_line + 1,
176                            end_column: lines[table_block.end_line].len() + 2,
177                            severity: Severity::Warning,
178                            fix: Some(Fix {
179                                // Insert blank lines at the end of the table's last line
180                                range: _line_index.line_col_to_byte_range(
181                                    table_block.end_line + 1,
182                                    lines[table_block.end_line].len() + 1,
183                                ),
184                                replacement: "\n".repeat(needed),
185                            }),
186                        });
187                    }
188                }
189            }
190        }
191
192        Ok(warnings)
193    }
194
195    fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
196        let content = ctx.content;
197        let _line_index = &ctx.line_index;
198
199        let mut warnings = self.check(ctx)?;
200        if warnings.is_empty() {
201            return Ok(content.to_string());
202        }
203
204        let lines: Vec<&str> = content.lines().collect();
205        let mut result = Vec::new();
206        let mut i = 0;
207
208        while i < lines.len() {
209            // Check for warnings about missing blank lines before table
210            let warning_before = warnings
211                .iter()
212                .position(|w| w.line == i + 1 && w.message.contains("before table"));
213
214            if let Some(idx) = warning_before {
215                let warning = &warnings[idx];
216                // Extract number of needed blank lines from the message or use config default
217                let needed_blanks = if warning.message.contains("Missing blank line before") {
218                    1
219                } else if let Some(start) = warning.message.find("Missing ") {
220                    if let Some(end) = warning.message.find(" blank lines before") {
221                        warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
222                    } else {
223                        1
224                    }
225                } else {
226                    1
227                };
228
229                // Add the required number of blank lines
230                for _ in 0..needed_blanks {
231                    result.push("".to_string());
232                }
233                warnings.remove(idx);
234            }
235
236            result.push(lines[i].to_string());
237
238            // Check for warnings about missing blank lines after table
239            let warning_after = warnings
240                .iter()
241                .position(|w| w.line == i + 1 && w.message.contains("after table"));
242
243            if let Some(idx) = warning_after {
244                let warning = &warnings[idx];
245                // Extract number of needed blank lines from the message or use config default
246                let needed_blanks = if warning.message.contains("Missing blank line after") {
247                    1
248                } else if let Some(start) = warning.message.find("Missing ") {
249                    if let Some(end) = warning.message.find(" blank lines after") {
250                        warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
251                    } else {
252                        1
253                    }
254                } else {
255                    1
256                };
257
258                // Add the required number of blank lines
259                for _ in 0..needed_blanks {
260                    result.push("".to_string());
261                }
262                warnings.remove(idx);
263            }
264
265            i += 1;
266        }
267
268        Ok(result.join("\n"))
269    }
270
271    fn as_any(&self) -> &dyn std::any::Any {
272        self
273    }
274
275    fn default_config_section(&self) -> Option<(String, toml::Value)> {
276        let default_config = MD058Config::default();
277        let json_value = serde_json::to_value(&default_config).ok()?;
278        let toml_value = crate::rule_config_serde::json_to_toml_value(&json_value)?;
279        if let toml::Value::Table(table) = toml_value {
280            if !table.is_empty() {
281                Some((MD058Config::RULE_NAME.to_string(), toml::Value::Table(table)))
282            } else {
283                None
284            }
285        } else {
286            None
287        }
288    }
289
290    fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
291    where
292        Self: Sized,
293    {
294        let rule_config = crate::rule_config_serde::load_rule_config::<MD058Config>(config);
295        Box::new(MD058BlanksAroundTables::from_config_struct(rule_config))
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use crate::lint_context::LintContext;
303    use crate::utils::table_utils::TableUtils;
304
305    #[test]
306    fn test_table_with_blanks() {
307        let rule = MD058BlanksAroundTables::default();
308        let content = "Some text before.
309
310| Header 1 | Header 2 |
311|----------|----------|
312| Cell 1   | Cell 2   |
313
314Some text after.";
315        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
316        let result = rule.check(&ctx).unwrap();
317
318        assert_eq!(result.len(), 0);
319    }
320
321    #[test]
322    fn test_table_missing_blank_before() {
323        let rule = MD058BlanksAroundTables::default();
324        let content = "Some text before.
325| Header 1 | Header 2 |
326|----------|----------|
327| Cell 1   | Cell 2   |
328
329Some text after.";
330        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
331        let result = rule.check(&ctx).unwrap();
332
333        assert_eq!(result.len(), 1);
334        assert_eq!(result[0].line, 2);
335        assert!(result[0].message.contains("Missing blank line before table"));
336    }
337
338    #[test]
339    fn test_table_missing_blank_after() {
340        let rule = MD058BlanksAroundTables::default();
341        let content = "Some text before.
342
343| Header 1 | Header 2 |
344|----------|----------|
345| Cell 1   | Cell 2   |
346Some text after.";
347        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
348        let result = rule.check(&ctx).unwrap();
349
350        assert_eq!(result.len(), 1);
351        assert_eq!(result[0].line, 5);
352        assert!(result[0].message.contains("Missing blank line after table"));
353    }
354
355    #[test]
356    fn test_table_missing_both_blanks() {
357        let rule = MD058BlanksAroundTables::default();
358        let content = "Some text before.
359| Header 1 | Header 2 |
360|----------|----------|
361| Cell 1   | Cell 2   |
362Some text after.";
363        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
364        let result = rule.check(&ctx).unwrap();
365
366        assert_eq!(result.len(), 2);
367        assert!(result[0].message.contains("Missing blank line before table"));
368        assert!(result[1].message.contains("Missing blank line after table"));
369    }
370
371    #[test]
372    fn test_table_at_start_of_document() {
373        let rule = MD058BlanksAroundTables::default();
374        let content = "| Header 1 | Header 2 |
375|----------|----------|
376| Cell 1   | Cell 2   |
377
378Some text after.";
379        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
380        let result = rule.check(&ctx).unwrap();
381
382        // No blank line needed before table at start of document
383        assert_eq!(result.len(), 0);
384    }
385
386    #[test]
387    fn test_table_at_end_of_document() {
388        let rule = MD058BlanksAroundTables::default();
389        let content = "Some text before.
390
391| Header 1 | Header 2 |
392|----------|----------|
393| Cell 1   | Cell 2   |";
394        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
395        let result = rule.check(&ctx).unwrap();
396
397        // No blank line needed after table at end of document
398        assert_eq!(result.len(), 0);
399    }
400
401    #[test]
402    fn test_multiple_tables() {
403        let rule = MD058BlanksAroundTables::default();
404        let content = "Text before first table.
405| Col 1 | Col 2 |
406|--------|-------|
407| Data 1 | Val 1 |
408Text between tables.
409| Col A | Col B |
410|--------|-------|
411| Data 2 | Val 2 |
412Text after second table.";
413        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
414        let result = rule.check(&ctx).unwrap();
415
416        assert_eq!(result.len(), 4);
417        // First table missing blanks
418        assert!(result[0].message.contains("Missing blank line before table"));
419        assert!(result[1].message.contains("Missing blank line after table"));
420        // Second table missing blanks
421        assert!(result[2].message.contains("Missing blank line before table"));
422        assert!(result[3].message.contains("Missing blank line after table"));
423    }
424
425    #[test]
426    fn test_consecutive_tables() {
427        let rule = MD058BlanksAroundTables::default();
428        let content = "Some text.
429
430| Col 1 | Col 2 |
431|--------|-------|
432| Data 1 | Val 1 |
433
434| Col A | Col B |
435|--------|-------|
436| Data 2 | Val 2 |
437
438More text.";
439        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
440        let result = rule.check(&ctx).unwrap();
441
442        // Tables separated by blank line should be OK
443        assert_eq!(result.len(), 0);
444    }
445
446    #[test]
447    fn test_consecutive_tables_no_blank() {
448        let rule = MD058BlanksAroundTables::default();
449        // Add a non-table line between tables to force detection as separate tables
450        let content = "Some text.
451
452| Col 1 | Col 2 |
453|--------|-------|
454| Data 1 | Val 1 |
455Text between.
456| Col A | Col B |
457|--------|-------|
458| Data 2 | Val 2 |
459
460More text.";
461        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
462        let result = rule.check(&ctx).unwrap();
463
464        // Should flag missing blanks around both tables
465        assert_eq!(result.len(), 2);
466        assert!(result[0].message.contains("Missing blank line after table"));
467        assert!(result[1].message.contains("Missing blank line before table"));
468    }
469
470    #[test]
471    fn test_fix_missing_blanks() {
472        let rule = MD058BlanksAroundTables::default();
473        let content = "Text before.
474| Header | Col 2 |
475|--------|-------|
476| Cell   | Data  |
477Text after.";
478        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
479        let fixed = rule.fix(&ctx).unwrap();
480
481        let expected = "Text before.
482
483| Header | Col 2 |
484|--------|-------|
485| Cell   | Data  |
486
487Text after.";
488        assert_eq!(fixed, expected);
489    }
490
491    #[test]
492    fn test_fix_multiple_tables() {
493        let rule = MD058BlanksAroundTables::default();
494        let content = "Start
495| T1 | C1 |
496|----|----|
497| D1 | V1 |
498Middle
499| T2 | C2 |
500|----|----|
501| D2 | V2 |
502End";
503        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
504        let fixed = rule.fix(&ctx).unwrap();
505
506        let expected = "Start
507
508| T1 | C1 |
509|----|----|
510| D1 | V1 |
511
512Middle
513
514| T2 | C2 |
515|----|----|
516| D2 | V2 |
517
518End";
519        assert_eq!(fixed, expected);
520    }
521
522    #[test]
523    fn test_empty_content() {
524        let rule = MD058BlanksAroundTables::default();
525        let content = "";
526        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
527        let result = rule.check(&ctx).unwrap();
528
529        assert_eq!(result.len(), 0);
530    }
531
532    #[test]
533    fn test_no_tables() {
534        let rule = MD058BlanksAroundTables::default();
535        let content = "Just regular text.
536No tables here.
537Only paragraphs.";
538        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
539        let result = rule.check(&ctx).unwrap();
540
541        assert_eq!(result.len(), 0);
542    }
543
544    #[test]
545    fn test_code_block_with_table() {
546        let rule = MD058BlanksAroundTables::default();
547        let content = "Text before.
548```
549| Not | A | Table |
550|-----|---|-------|
551| In  | Code | Block |
552```
553Text after.";
554        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
555        let result = rule.check(&ctx).unwrap();
556
557        // Tables in code blocks should be ignored
558        assert_eq!(result.len(), 0);
559    }
560
561    #[test]
562    fn test_table_with_complex_content() {
563        let rule = MD058BlanksAroundTables::default();
564        let content = "# Heading
565| Column 1 | Column 2 | Column 3 |
566|:---------|:--------:|---------:|
567| Left     | Center   | Right    |
568| Data     | More     | Info     |
569## Another Heading";
570        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
571        let result = rule.check(&ctx).unwrap();
572
573        assert_eq!(result.len(), 2);
574        assert!(result[0].message.contains("Missing blank line before table"));
575        assert!(result[1].message.contains("Missing blank line after table"));
576    }
577
578    #[test]
579    fn test_table_with_empty_cells() {
580        let rule = MD058BlanksAroundTables::default();
581        let content = "Text.
582
583|     |     |     |
584|-----|-----|-----|
585|     | X   |     |
586| O   |     | X   |
587
588More text.";
589        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
590        let result = rule.check(&ctx).unwrap();
591
592        assert_eq!(result.len(), 0);
593    }
594
595    #[test]
596    fn test_table_with_unicode() {
597        let rule = MD058BlanksAroundTables::default();
598        let content = "Unicode test.
599| 名前 | 年齢 | 都市 |
600|------|------|------|
601| 田中 | 25   | 東京 |
602| 佐藤 | 30   | 大阪 |
603End.";
604        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
605        let result = rule.check(&ctx).unwrap();
606
607        assert_eq!(result.len(), 2);
608    }
609
610    #[test]
611    fn test_table_with_long_cells() {
612        let rule = MD058BlanksAroundTables::default();
613        let content = "Before.
614
615| Short | Very very very very very very very very long header |
616|-------|-----------------------------------------------------|
617| Data  | This is an extremely long cell content that goes on |
618
619After.";
620        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
621        let result = rule.check(&ctx).unwrap();
622
623        assert_eq!(result.len(), 0);
624    }
625
626    #[test]
627    fn test_table_without_content_rows() {
628        let rule = MD058BlanksAroundTables::default();
629        let content = "Text.
630| Header 1 | Header 2 |
631|----------|----------|
632Next paragraph.";
633        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
634        let result = rule.check(&ctx).unwrap();
635
636        // Should still require blanks around header-only table
637        assert_eq!(result.len(), 2);
638    }
639
640    #[test]
641    fn test_indented_table() {
642        let rule = MD058BlanksAroundTables::default();
643        let content = "List item:
644
645    | Indented | Table |
646    |----------|-------|
647    | Data     | Here  |
648
649    More content.";
650        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
651        let result = rule.check(&ctx).unwrap();
652
653        // Indented tables should be detected
654        assert_eq!(result.len(), 0);
655    }
656
657    #[test]
658    fn test_single_column_table_not_detected() {
659        let rule = MD058BlanksAroundTables::default();
660        let content = "Text before.
661| Single |
662|--------|
663| Column |
664Text after.";
665        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
666        let result = rule.check(&ctx).unwrap();
667
668        // Single column tables ARE now detected (fixed to support 1+ columns)
669        // Expects 2 warnings: missing blank before and after table
670        assert_eq!(result.len(), 2);
671        assert!(result[0].message.contains("before"));
672        assert!(result[1].message.contains("after"));
673    }
674
675    #[test]
676    fn test_config_minimum_before() {
677        let config = MD058Config {
678            minimum_before: 2,
679            minimum_after: 1,
680        };
681        let rule = MD058BlanksAroundTables::from_config_struct(config);
682
683        let content = "Text before.
684
685| Header | Col 2 |
686|--------|-------|
687| Cell   | Data  |
688
689Text after.";
690        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
691        let result = rule.check(&ctx).unwrap();
692
693        // Should pass with 1 blank line before (but we configured to require 2)
694        assert_eq!(result.len(), 1);
695        assert!(result[0].message.contains("Missing 1 blank lines before table"));
696    }
697
698    #[test]
699    fn test_config_minimum_after() {
700        let config = MD058Config {
701            minimum_before: 1,
702            minimum_after: 3,
703        };
704        let rule = MD058BlanksAroundTables::from_config_struct(config);
705
706        let content = "Text before.
707
708| Header | Col 2 |
709|--------|-------|
710| Cell   | Data  |
711
712More text.";
713        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
714        let result = rule.check(&ctx).unwrap();
715
716        // Should fail with only 1 blank line after (but we configured to require 3)
717        assert_eq!(result.len(), 1);
718        assert!(result[0].message.contains("Missing 2 blank lines after table"));
719    }
720
721    #[test]
722    fn test_config_both_minimum() {
723        let config = MD058Config {
724            minimum_before: 2,
725            minimum_after: 2,
726        };
727        let rule = MD058BlanksAroundTables::from_config_struct(config);
728
729        let content = "Text before.
730| Header | Col 2 |
731|--------|-------|
732| Cell   | Data  |
733More text.";
734        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
735        let result = rule.check(&ctx).unwrap();
736
737        // Should fail both before and after
738        assert_eq!(result.len(), 2);
739        assert!(result[0].message.contains("Missing 2 blank lines before table"));
740        assert!(result[1].message.contains("Missing 2 blank lines after table"));
741    }
742
743    #[test]
744    fn test_config_zero_minimum() {
745        let config = MD058Config {
746            minimum_before: 0,
747            minimum_after: 0,
748        };
749        let rule = MD058BlanksAroundTables::from_config_struct(config);
750
751        let content = "Text before.
752| Header | Col 2 |
753|--------|-------|
754| Cell   | Data  |
755More text.";
756        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
757        let result = rule.check(&ctx).unwrap();
758
759        // Should pass with zero blank lines required
760        assert_eq!(result.len(), 0);
761    }
762
763    #[test]
764    fn test_fix_with_custom_config() {
765        let config = MD058Config {
766            minimum_before: 2,
767            minimum_after: 3,
768        };
769        let rule = MD058BlanksAroundTables::from_config_struct(config);
770
771        let content = "Text before.
772| Header | Col 2 |
773|--------|-------|
774| Cell   | Data  |
775Text after.";
776        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
777        let fixed = rule.fix(&ctx).unwrap();
778
779        let expected = "Text before.
780
781
782| Header | Col 2 |
783|--------|-------|
784| Cell   | Data  |
785
786
787
788Text after.";
789        assert_eq!(fixed, expected);
790    }
791
792    #[test]
793    fn test_default_config_section() {
794        let rule = MD058BlanksAroundTables::default();
795        let config_section = rule.default_config_section();
796
797        assert!(config_section.is_some());
798        let (name, value) = config_section.unwrap();
799        assert_eq!(name, "MD058");
800
801        // Should contain both minimum_before and minimum_after options with default values
802        if let toml::Value::Table(table) = value {
803            assert!(table.contains_key("minimum-before"));
804            assert!(table.contains_key("minimum-after"));
805            assert_eq!(table["minimum-before"], toml::Value::Integer(1));
806            assert_eq!(table["minimum-after"], toml::Value::Integer(1));
807        } else {
808            panic!("Expected TOML table");
809        }
810    }
811
812    #[test]
813    fn test_blank_lines_counting() {
814        let rule = MD058BlanksAroundTables::default();
815        let lines = vec!["text", "", "", "table", "more", "", "end"];
816
817        // Test counting blank lines before line index 3 (table)
818        assert_eq!(rule.count_blank_lines_before(&lines, 3), 2);
819
820        // Test counting blank lines after line index 4 (more)
821        assert_eq!(rule.count_blank_lines_after(&lines, 4), 1);
822
823        // Test at beginning
824        assert_eq!(rule.count_blank_lines_before(&lines, 0), 0);
825
826        // Test at end
827        assert_eq!(rule.count_blank_lines_after(&lines, 6), 0);
828    }
829
830    #[test]
831    fn test_issue_25_table_with_long_line() {
832        // Test case from issue #25 - table with very long line
833        let rule = MD058BlanksAroundTables::default();
834        let content = "# Title\n\nThis is a table:\n\n| Name          | Query                                                    |\n| ------------- | -------------------------------------------------------- |\n| b             | a                                                        |\n| c             | a                                                        |\n| d             | a                                                        |\n| long          | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |\n| e             | a                                                        |\n| f             | a                                                        |\n| g             | a                                                        |";
835        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
836
837        // Debug: Print detected table blocks
838        let table_blocks = TableUtils::find_table_blocks(content, &ctx);
839        for (i, block) in table_blocks.iter().enumerate() {
840            eprintln!(
841                "Table {}: start={}, end={}, header={}, delimiter={}, content_lines={:?}",
842                i + 1,
843                block.start_line + 1,
844                block.end_line + 1,
845                block.header_line + 1,
846                block.delimiter_line + 1,
847                block.content_lines.iter().map(|x| x + 1).collect::<Vec<_>>()
848            );
849        }
850
851        let result = rule.check(&ctx).unwrap();
852
853        // This should detect one table, not multiple tables
854        assert_eq!(table_blocks.len(), 1, "Should detect exactly one table block");
855
856        // Should not flag any issues since table is complete and doesn't need blanks
857        assert_eq!(result.len(), 0, "Should not flag any MD058 issues for a complete table");
858    }
859}