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