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