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