Skip to main content

rumdl_lib/rules/
md058_blanks_around_tables.rs

1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::rule_config_serde::RuleConfig;
3use crate::utils::mkdocs_attr_list::is_block_attribute_line;
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 (including blockquote continuation lines)
57    ///
58    /// Delegates to the shared `is_blank_in_blockquote_context` utility function.
59    /// This ensures consistent blank line detection across all rules that need
60    /// to handle blockquote-prefixed blank lines (MD058, MD065, etc.).
61    fn is_blank_line(&self, line: &str) -> bool {
62        crate::utils::regex_cache::is_blank_in_blockquote_context(line)
63    }
64
65    /// Count the number of blank lines before a given line index
66    fn count_blank_lines_before(&self, lines: &[&str], line_index: usize) -> usize {
67        let mut count = 0;
68        let mut i = line_index;
69        while i > 0 {
70            i -= 1;
71            if self.is_blank_line(lines[i]) {
72                count += 1;
73            } else {
74                break;
75            }
76        }
77        count
78    }
79
80    /// Count the number of blank lines after a given line index
81    fn count_blank_lines_after(&self, lines: &[&str], line_index: usize) -> usize {
82        let mut count = 0;
83        let mut i = line_index + 1;
84        while i < lines.len() {
85            if self.is_blank_line(lines[i]) {
86                count += 1;
87                i += 1;
88            } else {
89                break;
90            }
91        }
92        count
93    }
94}
95
96impl Rule for MD058BlanksAroundTables {
97    fn name(&self) -> &'static str {
98        "MD058"
99    }
100
101    fn description(&self) -> &'static str {
102        "Tables should be surrounded by blank lines"
103    }
104
105    fn category(&self) -> RuleCategory {
106        RuleCategory::Table
107    }
108
109    fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
110        // Skip if no tables present
111        !ctx.likely_has_tables()
112    }
113
114    fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
115        let content = ctx.content;
116        let line_index = &ctx.line_index;
117        let mut warnings = Vec::new();
118
119        // Early return for empty content or content without tables
120        if content.is_empty() || !content.contains('|') {
121            return Ok(Vec::new());
122        }
123
124        let lines = ctx.raw_lines();
125
126        // Use pre-computed table blocks from context
127        let table_blocks = &ctx.table_blocks;
128
129        for table_block in table_blocks {
130            // Check for sufficient blank lines before table
131            if table_block.start_line > 0 {
132                let blank_lines_before = self.count_blank_lines_before(lines, table_block.start_line);
133                if blank_lines_before < self.config.minimum_before {
134                    let needed = self.config.minimum_before - blank_lines_before;
135                    let message = if self.config.minimum_before == 1 {
136                        "Missing blank line before table".to_string()
137                    } else {
138                        format!("Missing {needed} blank lines before table")
139                    };
140
141                    let bq_prefix = ctx.blockquote_prefix_for_blank_line(table_block.start_line);
142                    let replacement = format!("{bq_prefix}\n").repeat(needed);
143                    warnings.push(LintWarning {
144                        rule_name: Some(self.name().to_string()),
145                        message,
146                        line: table_block.start_line + 1,
147                        column: 1,
148                        end_line: table_block.start_line + 1,
149                        end_column: 2,
150                        severity: Severity::Warning,
151                        fix: Some(Fix::new(
152                            line_index.line_col_to_byte_range(table_block.start_line + 1, 1),
153                            replacement,
154                        )),
155                    });
156                }
157            }
158
159            // Check for sufficient blank lines after table
160            if table_block.end_line < lines.len() - 1 {
161                // Check if the next line is a block attribute list attached to the table
162                let next_line_is_attribute = if table_block.end_line + 1 < lines.len() {
163                    is_block_attribute_line(lines[table_block.end_line + 1], ctx.flavor)
164                } else {
165                    false
166                };
167
168                // Skip check if next line is a block attribute
169                if !next_line_is_attribute {
170                    let blank_lines_after = self.count_blank_lines_after(lines, table_block.end_line);
171                    if blank_lines_after < self.config.minimum_after {
172                        let needed = self.config.minimum_after - blank_lines_after;
173                        let message = if self.config.minimum_after == 1 {
174                            "Missing blank line after table".to_string()
175                        } else {
176                            format!("Missing {needed} blank lines after table")
177                        };
178
179                        let bq_prefix = ctx.blockquote_prefix_for_blank_line(table_block.end_line);
180                        let replacement = format!("{bq_prefix}\n").repeat(needed);
181                        warnings.push(LintWarning {
182                            rule_name: Some(self.name().to_string()),
183                            message,
184                            line: table_block.end_line + 1,
185                            column: lines[table_block.end_line].chars().count() + 1,
186                            end_line: table_block.end_line + 1,
187                            end_column: lines[table_block.end_line].chars().count() + 2,
188                            severity: Severity::Warning,
189                            fix: Some(Fix::new(
190                                line_index.line_col_to_byte_range(
191                                    table_block.end_line + 1,
192                                    lines[table_block.end_line].len() + 1,
193                                ),
194                                replacement,
195                            )),
196                        });
197                    }
198                }
199            }
200        }
201
202        Ok(warnings)
203    }
204
205    fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
206        let content = ctx.content;
207
208        let warnings = self.check(ctx)?;
209        let mut warnings =
210            crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
211        if warnings.is_empty() {
212            return Ok(content.to_string());
213        }
214
215        let lines = ctx.raw_lines();
216        let mut result = Vec::new();
217        let mut i = 0;
218
219        while i < lines.len() {
220            // Check for warnings about missing blank lines before table
221            let warning_before = warnings
222                .iter()
223                .position(|w| w.line == i + 1 && w.message.contains("before table"));
224
225            if let Some(idx) = warning_before {
226                let warning = &warnings[idx];
227                // Extract number of needed blank lines from the message or use config default
228                let needed_blanks = if warning.message.contains("Missing blank line before") {
229                    1
230                } else if let Some(start) = warning.message.find("Missing ") {
231                    if let Some(end) = warning.message.find(" blank lines before") {
232                        warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
233                    } else {
234                        1
235                    }
236                } else {
237                    1
238                };
239
240                // Add the required number of blank lines with blockquote prefix
241                let bq_prefix = ctx.blockquote_prefix_for_blank_line(i);
242                for _ in 0..needed_blanks {
243                    result.push(bq_prefix.clone());
244                }
245                warnings.remove(idx);
246            }
247
248            result.push(lines[i].to_string());
249
250            // Check for warnings about missing blank lines after table
251            let warning_after = warnings
252                .iter()
253                .position(|w| w.line == i + 1 && w.message.contains("after table"));
254
255            if let Some(idx) = warning_after {
256                let warning = &warnings[idx];
257                // Extract number of needed blank lines from the message or use config default
258                let needed_blanks = if warning.message.contains("Missing blank line after") {
259                    1
260                } else if let Some(start) = warning.message.find("Missing ") {
261                    if let Some(end) = warning.message.find(" blank lines after") {
262                        warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
263                    } else {
264                        1
265                    }
266                } else {
267                    1
268                };
269
270                // Add the required number of blank lines with blockquote prefix
271                let bq_prefix = ctx.blockquote_prefix_for_blank_line(i);
272                for _ in 0..needed_blanks {
273                    result.push(bq_prefix.clone());
274                }
275                warnings.remove(idx);
276            }
277
278            i += 1;
279        }
280
281        let mut fixed = result.join("\n");
282        if content.ends_with('\n') {
283            fixed.push('\n');
284        }
285
286        Ok(fixed)
287    }
288
289    fn as_any(&self) -> &dyn std::any::Any {
290        self
291    }
292
293    crate::impl_rule_config_methods!(MD058Config);
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::lint_context::LintContext;
300    use crate::utils::table_utils::TableUtils;
301
302    #[test]
303    fn test_table_with_blanks() {
304        let rule = MD058BlanksAroundTables::default();
305        let content = "Some text before.
306
307| Header 1 | Header 2 |
308|----------|----------|
309| Cell 1   | Cell 2   |
310
311Some text after.";
312        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
313        let result = rule.check(&ctx).unwrap();
314
315        assert_eq!(result.len(), 0);
316    }
317
318    #[test]
319    fn test_table_missing_blank_before() {
320        let rule = MD058BlanksAroundTables::default();
321        let content = "Some text before.
322| Header 1 | Header 2 |
323|----------|----------|
324| Cell 1   | Cell 2   |
325
326Some text after.";
327        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
328        let result = rule.check(&ctx).unwrap();
329
330        assert_eq!(result.len(), 1);
331        assert_eq!(result[0].line, 2);
332        assert!(result[0].message.contains("Missing blank line before table"));
333    }
334
335    #[test]
336    fn test_table_missing_blank_after() {
337        let rule = MD058BlanksAroundTables::default();
338        let content = "Some text before.
339
340| Header 1 | Header 2 |
341|----------|----------|
342| Cell 1   | Cell 2   |
343Some text after.";
344        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
345        let result = rule.check(&ctx).unwrap();
346
347        assert_eq!(result.len(), 1);
348        assert_eq!(result[0].line, 5);
349        assert!(result[0].message.contains("Missing blank line after table"));
350    }
351
352    #[test]
353    fn test_table_missing_both_blanks() {
354        let rule = MD058BlanksAroundTables::default();
355        let content = "Some text before.
356| Header 1 | Header 2 |
357|----------|----------|
358| Cell 1   | Cell 2   |
359Some text after.";
360        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
361        let result = rule.check(&ctx).unwrap();
362
363        assert_eq!(result.len(), 2);
364        assert!(result[0].message.contains("Missing blank line before table"));
365        assert!(result[1].message.contains("Missing blank line after table"));
366    }
367
368    #[test]
369    fn test_table_at_start_of_document() {
370        let rule = MD058BlanksAroundTables::default();
371        let content = "| Header 1 | Header 2 |
372|----------|----------|
373| Cell 1   | Cell 2   |
374
375Some text after.";
376        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
377        let result = rule.check(&ctx).unwrap();
378
379        // No blank line needed before table at start of document
380        assert_eq!(result.len(), 0);
381    }
382
383    #[test]
384    fn test_table_at_end_of_document() {
385        let rule = MD058BlanksAroundTables::default();
386        let content = "Some text before.
387
388| Header 1 | Header 2 |
389|----------|----------|
390| Cell 1   | Cell 2   |";
391        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
392        let result = rule.check(&ctx).unwrap();
393
394        // No blank line needed after table at end of document
395        assert_eq!(result.len(), 0);
396    }
397
398    #[test]
399    fn test_multiple_tables() {
400        let rule = MD058BlanksAroundTables::default();
401        let content = "Text before first table.
402| Col 1 | Col 2 |
403|--------|-------|
404| Data 1 | Val 1 |
405Text between tables.
406| Col A | Col B |
407|--------|-------|
408| Data 2 | Val 2 |
409Text after second table.";
410        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
411        let result = rule.check(&ctx).unwrap();
412
413        assert_eq!(result.len(), 4);
414        // First table missing blanks
415        assert!(result[0].message.contains("Missing blank line before table"));
416        assert!(result[1].message.contains("Missing blank line after table"));
417        // Second table missing blanks
418        assert!(result[2].message.contains("Missing blank line before table"));
419        assert!(result[3].message.contains("Missing blank line after table"));
420    }
421
422    #[test]
423    fn test_consecutive_tables() {
424        let rule = MD058BlanksAroundTables::default();
425        let content = "Some text.
426
427| Col 1 | Col 2 |
428|--------|-------|
429| Data 1 | Val 1 |
430
431| Col A | Col B |
432|--------|-------|
433| Data 2 | Val 2 |
434
435More text.";
436        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
437        let result = rule.check(&ctx).unwrap();
438
439        // Tables separated by blank line should be OK
440        assert_eq!(result.len(), 0);
441    }
442
443    #[test]
444    fn test_consecutive_tables_no_blank() {
445        let rule = MD058BlanksAroundTables::default();
446        // Add a non-table line between tables to force detection as separate tables
447        let content = "Some text.
448
449| Col 1 | Col 2 |
450|--------|-------|
451| Data 1 | Val 1 |
452Text between.
453| Col A | Col B |
454|--------|-------|
455| Data 2 | Val 2 |
456
457More text.";
458        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
459        let result = rule.check(&ctx).unwrap();
460
461        // Should flag missing blanks around both tables
462        assert_eq!(result.len(), 2);
463        assert!(result[0].message.contains("Missing blank line after table"));
464        assert!(result[1].message.contains("Missing blank line before table"));
465    }
466
467    #[test]
468    fn test_fix_missing_blanks() {
469        let rule = MD058BlanksAroundTables::default();
470        let content = "Text before.
471| Header | Col 2 |
472|--------|-------|
473| Cell   | Data  |
474Text after.";
475        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
476        let fixed = rule.fix(&ctx).unwrap();
477
478        let expected = "Text before.
479
480| Header | Col 2 |
481|--------|-------|
482| Cell   | Data  |
483
484Text after.";
485        assert_eq!(fixed, expected);
486    }
487
488    #[test]
489    fn test_fix_multiple_tables() {
490        let rule = MD058BlanksAroundTables::default();
491        let content = "Start
492| T1 | C1 |
493|----|----|
494| D1 | V1 |
495Middle
496| T2 | C2 |
497|----|----|
498| D2 | V2 |
499End";
500        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
501        let fixed = rule.fix(&ctx).unwrap();
502
503        let expected = "Start
504
505| T1 | C1 |
506|----|----|
507| D1 | V1 |
508
509Middle
510
511| T2 | C2 |
512|----|----|
513| D2 | V2 |
514
515End";
516        assert_eq!(fixed, expected);
517    }
518
519    #[test]
520    fn test_empty_content() {
521        let rule = MD058BlanksAroundTables::default();
522        let content = "";
523        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
524        let result = rule.check(&ctx).unwrap();
525
526        assert_eq!(result.len(), 0);
527    }
528
529    #[test]
530    fn test_no_tables() {
531        let rule = MD058BlanksAroundTables::default();
532        let content = "Just regular text.
533No tables here.
534Only paragraphs.";
535        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
536        let result = rule.check(&ctx).unwrap();
537
538        assert_eq!(result.len(), 0);
539    }
540
541    #[test]
542    fn test_code_block_with_table() {
543        let rule = MD058BlanksAroundTables::default();
544        let content = "Text before.
545```
546| Not | A | Table |
547|-----|---|-------|
548| In  | Code | Block |
549```
550Text after.";
551        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
552        let result = rule.check(&ctx).unwrap();
553
554        // Tables in code blocks should be ignored
555        assert_eq!(result.len(), 0);
556    }
557
558    #[test]
559    fn test_table_with_complex_content() {
560        let rule = MD058BlanksAroundTables::default();
561        let content = "# Heading
562| Column 1 | Column 2 | Column 3 |
563|:---------|:--------:|---------:|
564| Left     | Center   | Right    |
565| Data     | More     | Info     |
566## Another Heading";
567        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
568        let result = rule.check(&ctx).unwrap();
569
570        assert_eq!(result.len(), 2);
571        assert!(result[0].message.contains("Missing blank line before table"));
572        assert!(result[1].message.contains("Missing blank line after table"));
573    }
574
575    #[test]
576    fn test_table_with_empty_cells() {
577        let rule = MD058BlanksAroundTables::default();
578        let content = "Text.
579
580|     |     |     |
581|-----|-----|-----|
582|     | X   |     |
583| O   |     | X   |
584
585More text.";
586        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
587        let result = rule.check(&ctx).unwrap();
588
589        assert_eq!(result.len(), 0);
590    }
591
592    #[test]
593    fn test_table_with_unicode() {
594        let rule = MD058BlanksAroundTables::default();
595        let content = "Unicode test.
596| 名前 | 年齢 | 都市 |
597|------|------|------|
598| 田中 | 25   | 東京 |
599| 佐藤 | 30   | 大阪 |
600End.";
601        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
602        let result = rule.check(&ctx).unwrap();
603
604        assert_eq!(result.len(), 2);
605    }
606
607    #[test]
608    fn test_table_with_long_cells() {
609        let rule = MD058BlanksAroundTables::default();
610        let content = "Before.
611
612| Short | Very very very very very very very very long header |
613|-------|-----------------------------------------------------|
614| Data  | This is an extremely long cell content that goes on |
615
616After.";
617        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
618        let result = rule.check(&ctx).unwrap();
619
620        assert_eq!(result.len(), 0);
621    }
622
623    #[test]
624    fn test_table_without_content_rows() {
625        let rule = MD058BlanksAroundTables::default();
626        let content = "Text.
627| Header 1 | Header 2 |
628|----------|----------|
629Next paragraph.";
630        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
631        let result = rule.check(&ctx).unwrap();
632
633        // Should still require blanks around header-only table
634        assert_eq!(result.len(), 2);
635    }
636
637    #[test]
638    fn test_indented_table() {
639        let rule = MD058BlanksAroundTables::default();
640        let content = "List item:
641
642    | Indented | Table |
643    |----------|-------|
644    | Data     | Here  |
645
646    More content.";
647        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
648        let result = rule.check(&ctx).unwrap();
649
650        // Indented tables should be detected
651        assert_eq!(result.len(), 0);
652    }
653
654    #[test]
655    fn test_single_column_table_not_detected() {
656        let rule = MD058BlanksAroundTables::default();
657        let content = "Text before.
658| Single |
659|--------|
660| Column |
661Text after.";
662        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
663        let result = rule.check(&ctx).unwrap();
664
665        // Single column tables ARE now detected (fixed to support 1+ columns)
666        // Expects 2 warnings: missing blank before and after table
667        assert_eq!(result.len(), 2);
668        assert!(result[0].message.contains("before"));
669        assert!(result[1].message.contains("after"));
670    }
671
672    #[test]
673    fn test_config_minimum_before() {
674        let config = MD058Config {
675            minimum_before: 2,
676            minimum_after: 1,
677        };
678        let rule = MD058BlanksAroundTables::from_config_struct(config);
679
680        let content = "Text before.
681
682| Header | Col 2 |
683|--------|-------|
684| Cell   | Data  |
685
686Text after.";
687        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
688        let result = rule.check(&ctx).unwrap();
689
690        // Should pass with 1 blank line before (but we configured to require 2)
691        assert_eq!(result.len(), 1);
692        assert!(result[0].message.contains("Missing 1 blank lines before table"));
693    }
694
695    #[test]
696    fn test_config_minimum_after() {
697        let config = MD058Config {
698            minimum_before: 1,
699            minimum_after: 3,
700        };
701        let rule = MD058BlanksAroundTables::from_config_struct(config);
702
703        let content = "Text before.
704
705| Header | Col 2 |
706|--------|-------|
707| Cell   | Data  |
708
709More text.";
710        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
711        let result = rule.check(&ctx).unwrap();
712
713        // Should fail with only 1 blank line after (but we configured to require 3)
714        assert_eq!(result.len(), 1);
715        assert!(result[0].message.contains("Missing 2 blank lines after table"));
716    }
717
718    #[test]
719    fn test_config_both_minimum() {
720        let config = MD058Config {
721            minimum_before: 2,
722            minimum_after: 2,
723        };
724        let rule = MD058BlanksAroundTables::from_config_struct(config);
725
726        let content = "Text before.
727| Header | Col 2 |
728|--------|-------|
729| Cell   | Data  |
730More text.";
731        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
732        let result = rule.check(&ctx).unwrap();
733
734        // Should fail both before and after
735        assert_eq!(result.len(), 2);
736        assert!(result[0].message.contains("Missing 2 blank lines before table"));
737        assert!(result[1].message.contains("Missing 2 blank lines after table"));
738    }
739
740    #[test]
741    fn test_config_zero_minimum() {
742        let config = MD058Config {
743            minimum_before: 0,
744            minimum_after: 0,
745        };
746        let rule = MD058BlanksAroundTables::from_config_struct(config);
747
748        let content = "Text before.
749| Header | Col 2 |
750|--------|-------|
751| Cell   | Data  |
752More text.";
753        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
754        let result = rule.check(&ctx).unwrap();
755
756        // Should pass with zero blank lines required
757        assert_eq!(result.len(), 0);
758    }
759
760    #[test]
761    fn test_fix_with_custom_config() {
762        let config = MD058Config {
763            minimum_before: 2,
764            minimum_after: 3,
765        };
766        let rule = MD058BlanksAroundTables::from_config_struct(config);
767
768        let content = "Text before.
769| Header | Col 2 |
770|--------|-------|
771| Cell   | Data  |
772Text after.";
773        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
774        let fixed = rule.fix(&ctx).unwrap();
775
776        let expected = "Text before.
777
778
779| Header | Col 2 |
780|--------|-------|
781| Cell   | Data  |
782
783
784
785Text after.";
786        assert_eq!(fixed, expected);
787    }
788
789    #[test]
790    fn test_default_config_section() {
791        let rule = MD058BlanksAroundTables::default();
792        let config_section = rule.default_config_section();
793
794        assert!(config_section.is_some());
795        let (name, value) = config_section.unwrap();
796        assert_eq!(name, "MD058");
797
798        // Should contain both minimum_before and minimum_after options with default values
799        if let toml::Value::Table(table) = value {
800            assert!(table.contains_key("minimum-before"));
801            assert!(table.contains_key("minimum-after"));
802            assert_eq!(table["minimum-before"], toml::Value::Integer(1));
803            assert_eq!(table["minimum-after"], toml::Value::Integer(1));
804        } else {
805            panic!("Expected TOML table");
806        }
807    }
808
809    #[test]
810    fn test_blank_lines_counting() {
811        let rule = MD058BlanksAroundTables::default();
812        let lines = vec!["text", "", "", "table", "more", "", "end"];
813
814        // Test counting blank lines before line index 3 (table)
815        assert_eq!(rule.count_blank_lines_before(&lines, 3), 2);
816
817        // Test counting blank lines after line index 4 (more)
818        assert_eq!(rule.count_blank_lines_after(&lines, 4), 1);
819
820        // Test at beginning
821        assert_eq!(rule.count_blank_lines_before(&lines, 0), 0);
822
823        // Test at end
824        assert_eq!(rule.count_blank_lines_after(&lines, 6), 0);
825    }
826
827    #[test]
828    fn test_issue_25_table_with_long_line() {
829        // Test case from issue #25 - table with very long line
830        let rule = MD058BlanksAroundTables::default();
831        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                                                        |";
832        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
833
834        // Debug: Print detected table blocks
835        let table_blocks = TableUtils::find_table_blocks(content, &ctx);
836        for (i, block) in table_blocks.iter().enumerate() {
837            eprintln!(
838                "Table {}: start={}, end={}, header={}, delimiter={}, content_lines={:?}",
839                i + 1,
840                block.start_line + 1,
841                block.end_line + 1,
842                block.header_line + 1,
843                block.delimiter_line + 1,
844                block.content_lines.iter().map(|x| x + 1).collect::<Vec<_>>()
845            );
846        }
847
848        let result = rule.check(&ctx).unwrap();
849
850        // This should detect one table, not multiple tables
851        assert_eq!(table_blocks.len(), 1, "Should detect exactly one table block");
852
853        // Should not flag any issues since table is complete and doesn't need blanks
854        assert_eq!(result.len(), 0, "Should not flag any MD058 issues for a complete table");
855    }
856
857    #[test]
858    fn test_fix_preserves_blockquote_prefix_before_table() {
859        // Issue #268: Fix should insert blockquote-prefixed blank lines inside blockquotes
860        let rule = MD058BlanksAroundTables::default();
861
862        let content = "> Text before
863> | H1 | H2 |
864> |----|---|
865> | a  | b |";
866        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
867        let fixed = rule.fix(&ctx).unwrap();
868
869        // The blank line inserted before the table should have the blockquote prefix
870        let expected = "> Text before
871>
872> | H1 | H2 |
873> |----|---|
874> | a  | b |";
875        assert_eq!(
876            fixed, expected,
877            "Fix should insert '>' blank line before table, not plain blank line"
878        );
879    }
880
881    #[test]
882    fn test_fix_preserves_blockquote_prefix_after_table() {
883        // Issue #268: Fix should insert blockquote-prefixed blank lines inside blockquotes
884        let rule = MD058BlanksAroundTables::default();
885
886        let content = "> | H1 | H2 |
887> |----|---|
888> | a  | b |
889> Text after";
890        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
891        let fixed = rule.fix(&ctx).unwrap();
892
893        // The blank line inserted after the table should have the blockquote prefix
894        let expected = "> | H1 | H2 |
895> |----|---|
896> | a  | b |
897>
898> Text after";
899        assert_eq!(
900            fixed, expected,
901            "Fix should insert '>' blank line after table, not plain blank line"
902        );
903    }
904
905    #[test]
906    fn test_fix_preserves_nested_blockquote_prefix_for_table() {
907        // Nested blockquotes should preserve the full prefix
908        let rule = MD058BlanksAroundTables::default();
909
910        let content = ">> Nested quote
911>> | H1 |
912>> |----|
913>> | a  |
914>> More text";
915        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
916        let fixed = rule.fix(&ctx).unwrap();
917
918        // Should insert ">>" blank lines
919        let expected = ">> Nested quote
920>>
921>> | H1 |
922>> |----|
923>> | a  |
924>>
925>> More text";
926        assert_eq!(fixed, expected, "Fix should preserve nested blockquote prefix '>>'");
927    }
928
929    #[test]
930    fn test_fix_preserves_triple_nested_blockquote_prefix_for_table() {
931        // Triple-nested blockquotes should preserve full prefix
932        let rule = MD058BlanksAroundTables::default();
933
934        let content = ">>> Triple nested
935>>> | A | B |
936>>> |---|---|
937>>> | 1 | 2 |
938>>> More text";
939        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
940        let fixed = rule.fix(&ctx).unwrap();
941
942        let expected = ">>> Triple nested
943>>>
944>>> | A | B |
945>>> |---|---|
946>>> | 1 | 2 |
947>>>
948>>> More text";
949        assert_eq!(
950            fixed, expected,
951            "Fix should preserve triple-nested blockquote prefix '>>>'"
952        );
953    }
954
955    // =========================================================================
956    // Issue #305: Tables inside blockquotes with existing blank lines
957    // These tests verify that MD058 correctly recognizes blockquote continuation
958    // lines (e.g., ">") as "blank" lines for table spacing purposes.
959    // =========================================================================
960
961    #[test]
962    fn test_is_blank_line_with_blockquote_continuation() {
963        // Unit tests for is_blank_line recognizing blockquote blanks
964        let rule = MD058BlanksAroundTables::default();
965
966        // Regular blank lines
967        assert!(rule.is_blank_line(""));
968        assert!(rule.is_blank_line("   "));
969        assert!(rule.is_blank_line("\t"));
970        assert!(rule.is_blank_line("  \t  "));
971
972        // Blockquote continuation lines (should be treated as blank)
973        assert!(rule.is_blank_line(">"));
974        assert!(rule.is_blank_line("> "));
975        assert!(rule.is_blank_line(">  "));
976        assert!(rule.is_blank_line(">>"));
977        assert!(rule.is_blank_line(">> "));
978        assert!(rule.is_blank_line(">>>"));
979        assert!(rule.is_blank_line("> > "));
980        assert!(rule.is_blank_line("> > > "));
981        assert!(rule.is_blank_line("  >  ")); // With leading/trailing whitespace
982
983        // Lines with content (should NOT be treated as blank)
984        assert!(!rule.is_blank_line("text"));
985        assert!(!rule.is_blank_line("> text"));
986        assert!(!rule.is_blank_line(">> text"));
987        assert!(!rule.is_blank_line("> | table |"));
988        assert!(!rule.is_blank_line("| table |"));
989    }
990
991    #[test]
992    fn test_issue_305_no_warning_blockquote_with_existing_blank_before_table() {
993        // Issue #305: Table inside blockquote with existing blank line before
994        // should NOT trigger MD058 warning
995        let rule = MD058BlanksAroundTables::default();
996
997        let content = "> Text before
998>
999> | H1 | H2 |
1000> |----|---|
1001> | a  | b |";
1002        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1003        let result = rule.check(&ctx).unwrap();
1004
1005        assert_eq!(
1006            result.len(),
1007            0,
1008            "Should not warn when blockquote already has blank line before table"
1009        );
1010    }
1011
1012    #[test]
1013    fn test_issue_305_no_warning_blockquote_with_existing_blank_after_table() {
1014        // Issue #305: Table inside blockquote with existing blank line after
1015        // should NOT trigger MD058 warning
1016        let rule = MD058BlanksAroundTables::default();
1017
1018        let content = "> | H1 | H2 |
1019> |----|---|
1020> | a  | b |
1021>
1022> Text after";
1023        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1024        let result = rule.check(&ctx).unwrap();
1025
1026        assert_eq!(
1027            result.len(),
1028            0,
1029            "Should not warn when blockquote already has blank line after table"
1030        );
1031    }
1032
1033    #[test]
1034    fn test_issue_305_no_warning_blockquote_with_both_blank_lines() {
1035        // Issue #305: Complete example from the issue report
1036        let rule = MD058BlanksAroundTables::default();
1037
1038        let content = "> The following options are available:
1039>
1040> | Option | Default   | Description       |
1041> |--------|-----------|-------------------|
1042> | port   | 3000      | Server port       |
1043> | host   | localhost | Server host       |";
1044        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1045        let result = rule.check(&ctx).unwrap();
1046
1047        assert_eq!(
1048            result.len(),
1049            0,
1050            "Issue #305: Should not warn for valid table inside blockquote with blank line"
1051        );
1052    }
1053
1054    #[test]
1055    fn test_issue_305_no_warning_nested_blockquote_with_blank_lines() {
1056        // Nested blockquote with blank lines should not warn
1057        let rule = MD058BlanksAroundTables::default();
1058
1059        let content = ">> Nested text
1060>>
1061>> | Col1 | Col2 |
1062>> |------|------|
1063>> | val1 | val2 |
1064>>
1065>> More text";
1066        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1067        let result = rule.check(&ctx).unwrap();
1068
1069        assert_eq!(
1070            result.len(),
1071            0,
1072            "Should not warn for nested blockquote table with blank lines"
1073        );
1074    }
1075
1076    #[test]
1077    fn test_issue_305_no_warning_triple_nested_blockquote_with_blank_lines() {
1078        // Triple-nested blockquote with blank lines should not warn
1079        let rule = MD058BlanksAroundTables::default();
1080
1081        let content = ">>> Deep nesting
1082>>>
1083>>> | A | B |
1084>>> |---|---|
1085>>> | 1 | 2 |
1086>>>
1087>>> End";
1088        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1089        let result = rule.check(&ctx).unwrap();
1090
1091        assert_eq!(
1092            result.len(),
1093            0,
1094            "Should not warn for triple-nested blockquote table with blank lines"
1095        );
1096    }
1097
1098    #[test]
1099    fn test_issue_305_fix_does_not_corrupt_valid_blockquote_table() {
1100        // Critical: Verify that fix() doesn't corrupt already-valid content
1101        let rule = MD058BlanksAroundTables::default();
1102
1103        let content = "> Text before
1104>
1105> | H1 | H2 |
1106> |----|---|
1107> | a  | b |
1108>
1109> Text after";
1110        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1111        let fixed = rule.fix(&ctx).unwrap();
1112
1113        assert_eq!(fixed, content, "Fix should not modify already-valid blockquote table");
1114    }
1115
1116    #[test]
1117    fn test_issue_305_blockquote_blank_with_trailing_space() {
1118        // Blockquote blank line with trailing space ("> ") should be recognized
1119        let rule = MD058BlanksAroundTables::default();
1120
1121        // Note: The "> " has a trailing space
1122        let content = "> Text before
1123>
1124> | H1 | H2 |
1125> |----|---|
1126> | a  | b |";
1127        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1128        let result = rule.check(&ctx).unwrap();
1129
1130        assert_eq!(
1131            result.len(),
1132            0,
1133            "Should recognize '> ' (with trailing space) as blank line"
1134        );
1135    }
1136
1137    #[test]
1138    fn test_issue_305_spaced_nested_blockquote() {
1139        // "> > " style nested blockquote should be recognized
1140        let rule = MD058BlanksAroundTables::default();
1141
1142        let content = "> > Nested text
1143> >
1144> > | H1 |
1145> > |----|
1146> > | a  |";
1147        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1148        let result = rule.check(&ctx).unwrap();
1149
1150        assert_eq!(
1151            result.len(),
1152            0,
1153            "Should recognize '> > ' style nested blockquote blank line"
1154        );
1155    }
1156
1157    #[test]
1158    fn test_mixed_regular_and_blockquote_tables() {
1159        // Document with both regular tables and blockquote tables
1160        let rule = MD058BlanksAroundTables::default();
1161
1162        let content = "# Mixed Content
1163
1164Regular table:
1165
1166| A | B |
1167|---|---|
1168| 1 | 2 |
1169
1170And a blockquote table:
1171
1172> Quote text
1173>
1174> | X | Y |
1175> |---|---|
1176> | 3 | 4 |
1177>
1178> End quote
1179
1180Final paragraph.";
1181        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1182        let result = rule.check(&ctx).unwrap();
1183
1184        assert_eq!(
1185            result.len(),
1186            0,
1187            "Should handle mixed regular and blockquote tables correctly"
1188        );
1189    }
1190
1191    #[test]
1192    fn test_blockquote_table_at_document_start() {
1193        // Table in blockquote at very start of document
1194        let rule = MD058BlanksAroundTables::default();
1195
1196        let content = "> | H1 | H2 |
1197> |----|---|
1198> | a  | b |
1199>
1200> Text after";
1201        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1202        let result = rule.check(&ctx).unwrap();
1203
1204        assert_eq!(
1205            result.len(),
1206            0,
1207            "Should not require blank line before table at document start (even in blockquote)"
1208        );
1209    }
1210
1211    #[test]
1212    fn test_blockquote_table_at_document_end() {
1213        // Table in blockquote at very end of document
1214        let rule = MD058BlanksAroundTables::default();
1215
1216        let content = "> Text before
1217>
1218> | H1 | H2 |
1219> |----|---|
1220> | a  | b |";
1221        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1222        let result = rule.check(&ctx).unwrap();
1223
1224        assert_eq!(
1225            result.len(),
1226            0,
1227            "Should not require blank line after table at document end"
1228        );
1229    }
1230
1231    #[test]
1232    fn test_blockquote_table_missing_blank_still_detected() {
1233        // Ensure we still detect ACTUAL missing blank lines in blockquotes
1234        let rule = MD058BlanksAroundTables::default();
1235
1236        let content = "> Text before
1237> | H1 | H2 |
1238> |----|---|
1239> | a  | b |
1240> Text after";
1241        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1242        let result = rule.check(&ctx).unwrap();
1243
1244        // Should have 2 warnings: missing blank before AND after table
1245        assert_eq!(
1246            result.len(),
1247            2,
1248            "Should still detect missing blank lines in blockquote tables"
1249        );
1250        assert!(result[0].message.contains("before table"));
1251        assert!(result[1].message.contains("after table"));
1252    }
1253
1254    #[test]
1255    fn test_blockquote_table_fix_adds_correct_prefix() {
1256        // Verify fix adds blockquote-prefixed blank lines when needed
1257        let rule = MD058BlanksAroundTables::default();
1258
1259        let content = "> Text before
1260> | H1 | H2 |
1261> |----|---|
1262> | a  | b |
1263> Text after";
1264        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1265        let fixed = rule.fix(&ctx).unwrap();
1266
1267        let expected = "> Text before
1268>
1269> | H1 | H2 |
1270> |----|---|
1271> | a  | b |
1272>
1273> Text after";
1274        assert_eq!(fixed, expected, "Fix should add blockquote-prefixed blank lines");
1275    }
1276
1277    #[test]
1278    fn test_multiple_blockquote_tables_with_valid_spacing() {
1279        // Multiple tables in same blockquote, all with proper spacing
1280        let rule = MD058BlanksAroundTables::default();
1281
1282        let content = "> First table:
1283>
1284> | A | B |
1285> |---|---|
1286> | 1 | 2 |
1287>
1288> Second table:
1289>
1290> | X | Y |
1291> |---|---|
1292> | 3 | 4 |
1293>
1294> End";
1295        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1296        let result = rule.check(&ctx).unwrap();
1297
1298        assert_eq!(
1299            result.len(),
1300            0,
1301            "Should handle multiple blockquote tables with valid spacing"
1302        );
1303    }
1304
1305    #[test]
1306    fn test_blockquote_table_with_minimum_before_config() {
1307        // Test with custom minimum_before config
1308        let config = MD058Config {
1309            minimum_before: 2,
1310            minimum_after: 1,
1311        };
1312        let rule = MD058BlanksAroundTables::from_config_struct(config);
1313
1314        let content = "> Text
1315>
1316> | H1 |
1317> |----|
1318> | a  |";
1319        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1320        let result = rule.check(&ctx).unwrap();
1321
1322        // Should warn because only 1 blank line, but config requires 2
1323        assert_eq!(result.len(), 1);
1324        assert!(result[0].message.contains("before table"));
1325    }
1326
1327    // === Pandoc construct reachability tests ===
1328    //
1329    // These tests document that MD058 does not flag Pandoc-specific constructs
1330    // because `ctx.table_blocks` excludes them at the source:
1331    //
1332    // - Grid table delimiters use `+---+---+` (no `|`), so `is_delimiter_row`
1333    //   returns false and no `TableBlock` is created.
1334    // - Multi-line table separators have no `|`, same exclusion.
1335    // - Line blocks (`| First line`) end without `|`; `is_potential_table_row`
1336    //   requires `valid_parts >= 2` for non-outer-piped lines (only 1 found).
1337    // - Pipe-table captions (`: caption`) have no `|` — excluded.
1338    //
1339    // No production guard is needed. If `find_table_blocks` ever changes to
1340    // include these constructs, these tests will surface that.
1341
1342    #[test]
1343    fn md058_pandoc_grid_tables_not_flagged() {
1344        let rule = MD058BlanksAroundTables::default();
1345        // Grid table with no surrounding blank lines.
1346        // If grid tables were in table_blocks, MD058 would flag missing blanks.
1347        let content = "Some text before.
1348+---+---+
1349| a | b |
1350+===+===+
1351| 1 | 2 |
1352+---+---+
1353Some text after.";
1354
1355        // Under Pandoc: grid tables excluded from table_blocks — no warnings.
1356        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1357        let result = rule.check(&ctx).unwrap();
1358        assert!(
1359            result.is_empty(),
1360            "MD058 should not flag blank lines around Pandoc grid tables (excluded by table_blocks): {result:?}"
1361        );
1362
1363        // Under Standard: same content — grid table not recognized as pipe table.
1364        let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1365        let result_std = rule.check(&ctx_std).unwrap();
1366        assert!(
1367            result_std.is_empty(),
1368            "MD058 should not flag grid-table-like content under Standard: {result_std:?}"
1369        );
1370    }
1371
1372    #[test]
1373    fn md058_pandoc_multi_line_tables_not_flagged() {
1374        let rule = MD058BlanksAroundTables::default();
1375        let content = "Some text.
1376--------- -----------
1377Header 1   Header 2
1378--------- -----------
1379Cell 1     Cell 2
1380--------- -----------
1381More text.";
1382
1383        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1384        let result = rule.check(&ctx).unwrap();
1385        assert!(
1386            result.is_empty(),
1387            "MD058 should not flag Pandoc multi-line tables: {result:?}"
1388        );
1389
1390        let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1391        let result_std = rule.check(&ctx_std).unwrap();
1392        assert!(
1393            result_std.is_empty(),
1394            "MD058 should not flag multi-line table content under Standard: {result_std:?}"
1395        );
1396    }
1397
1398    #[test]
1399    fn md058_pandoc_line_blocks_not_flagged() {
1400        let rule = MD058BlanksAroundTables::default();
1401        // Pandoc line blocks are not recognized as pipe tables (no trailing `|`).
1402        let content = "Some text.
1403| First line
1404| Second line
1405More text.";
1406
1407        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1408        let result = rule.check(&ctx).unwrap();
1409        assert!(
1410            result.is_empty(),
1411            "MD058 should not treat Pandoc line blocks as tables: {result:?}"
1412        );
1413
1414        let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1415        let result_std = rule.check(&ctx_std).unwrap();
1416        assert!(
1417            result_std.is_empty(),
1418            "MD058 should not treat line-block-like content as tables under Standard: {result_std:?}"
1419        );
1420    }
1421
1422    #[test]
1423    fn md058_pandoc_pipe_table_captions_not_flagged() {
1424        let rule = MD058BlanksAroundTables::default();
1425        // Pipe-table captions (`: caption`) have no `|` — they are not table rows
1426        // and are never included in table_blocks, so MD058 ignores them.
1427        let content = "\
1428Some text.
1429
1430| H1 | H2 |
1431|----|-----|
1432| a  | b  |
1433
1434: My table caption
1435More text.";
1436
1437        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1438        let result = rule.check(&ctx).unwrap();
1439        assert!(
1440            result.is_empty(),
1441            "MD058 should not flag the pipe-table caption line as needing blank lines: {result:?}"
1442        );
1443
1444        // Under Standard: caption line has no `|` — excluded from table_blocks.
1445        // Table itself has proper blanks before and after.
1446        let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1447        let result_std = rule.check(&ctx_std).unwrap();
1448        assert!(
1449            result_std.is_empty(),
1450            "MD058 table with caption — caption not a table row under Standard: {result_std:?}"
1451        );
1452    }
1453
1454    #[test]
1455    fn md058_hugo_block_attribute_after_table_not_flagged() {
1456        // Issue #756: a Goldmark/Hugo block attribute list directly under a table
1457        // describes that table, so MD058 must not treat it as content needing a blank.
1458        let rule = MD058BlanksAroundTables::default();
1459        let content = "\
1460Some text.
1461
1462| H1 | H2 |
1463|----|-----|
1464| a  | b  |
1465{class=\"table table-striped\"}
1466
1467More text.";
1468
1469        for flavor in [
1470            crate::config::MarkdownFlavor::Hugo,
1471            crate::config::MarkdownFlavor::MkDocs,
1472            crate::config::MarkdownFlavor::Kramdown,
1473        ] {
1474            let ctx = LintContext::new(content, flavor, None);
1475            let result = rule.check(&ctx).unwrap();
1476            assert!(
1477                result.is_empty(),
1478                "MD058 should not flag the block attribute line under {flavor:?}: {result:?}"
1479            );
1480        }
1481
1482        // Negative control: in plain CommonMark `{class="a"}` is literal text, so the
1483        // table genuinely lacks a blank line after it.
1484        let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1485        let result_std = rule.check(&ctx_std).unwrap();
1486        assert_eq!(
1487            result_std.len(),
1488            1,
1489            "MD058 must flag the missing blank after table under Standard: {result_std:?}"
1490        );
1491        assert!(result_std[0].message.contains("Missing blank line after table"));
1492    }
1493
1494    #[test]
1495    fn test_fix_preserves_trailing_newline() {
1496        let rule = MD058BlanksAroundTables::default();
1497
1498        let content = "Intro\n| a | b |\n| --- | --- |\n| 1 | 2 |\nAfter\n";
1499        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1500        let fixed = rule.fix(&ctx).unwrap();
1501
1502        assert!(fixed.ends_with('\n'), "Fix should preserve trailing newline");
1503        assert_eq!(fixed, "Intro\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n\nAfter\n");
1504    }
1505
1506    #[test]
1507    fn test_fix_preserves_no_trailing_newline() {
1508        let rule = MD058BlanksAroundTables::default();
1509
1510        let content = "Intro\n| a | b |\n| --- | --- |\n| 1 | 2 |\nAfter";
1511        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1512        let fixed = rule.fix(&ctx).unwrap();
1513
1514        assert!(
1515            !fixed.ends_with('\n'),
1516            "Fix should not add trailing newline if original didn't have one"
1517        );
1518        assert_eq!(fixed, "Intro\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n\nAfter");
1519    }
1520}