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