rumdl_lib/rules/
md056_table_column_count.rs

1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, Severity};
2use crate::utils::range_utils::calculate_line_range;
3use crate::utils::table_utils::TableUtils;
4
5/// Rule MD056: Table column count
6///
7/// See [docs/md056.md](../../docs/md056.md) for full documentation, configuration, and examples.
8/// Ensures all rows in a table have the same number of cells
9#[derive(Debug, Clone)]
10pub struct MD056TableColumnCount;
11
12impl Default for MD056TableColumnCount {
13    fn default() -> Self {
14        MD056TableColumnCount
15    }
16}
17
18impl MD056TableColumnCount {
19    /// Try to fix a table row to match the expected column count
20    fn fix_table_row(&self, row: &str, expected_count: usize, flavor: crate::config::MarkdownFlavor) -> Option<String> {
21        // Extract blockquote prefix if present
22        let (prefix, content) = TableUtils::extract_blockquote_prefix(row);
23
24        let current_count = TableUtils::count_cells_with_flavor(content, flavor);
25
26        if current_count == expected_count || current_count == 0 {
27            return None;
28        }
29
30        // For standard flavor with too many cells, first try escaping pipes in inline code.
31        // This preserves content and produces valid GitHub-compatible output.
32        if flavor == crate::config::MarkdownFlavor::Standard && current_count > expected_count {
33            let escaped_row = TableUtils::escape_pipes_in_inline_code(content);
34            let escaped_count = TableUtils::count_cells_with_flavor(&escaped_row, flavor);
35
36            // If escaping pipes in inline code fixes the cell count, use that
37            if escaped_count == expected_count {
38                let fixed = escaped_row.trim().to_string();
39                return Some(if prefix.is_empty() {
40                    fixed
41                } else {
42                    format!("{prefix}{fixed}")
43                });
44            }
45
46            // If escaping reduced cell count, continue fixing with escaped version
47            if escaped_count < current_count {
48                let fixed = self.fix_row_by_truncation(&escaped_row, expected_count, flavor)?;
49                return Some(if prefix.is_empty() {
50                    fixed
51                } else {
52                    format!("{prefix}{fixed}")
53                });
54            }
55        }
56
57        let fixed = self.fix_row_by_truncation(content, expected_count, flavor)?;
58        Some(if prefix.is_empty() {
59            fixed
60        } else {
61            format!("{prefix}{fixed}")
62        })
63    }
64
65    /// Fix a table row by truncating or adding cells
66    fn fix_row_by_truncation(
67        &self,
68        row: &str,
69        expected_count: usize,
70        flavor: crate::config::MarkdownFlavor,
71    ) -> Option<String> {
72        let current_count = TableUtils::count_cells_with_flavor(row, flavor);
73
74        if current_count == expected_count || current_count == 0 {
75            return None;
76        }
77
78        let trimmed = row.trim();
79        let has_leading_pipe = trimmed.starts_with('|');
80        let has_trailing_pipe = trimmed.ends_with('|');
81
82        // Use flavor-aware cell splitting
83        let cells = Self::split_row_into_cells(trimmed, flavor);
84
85        let mut cell_contents: Vec<&str> = Vec::new();
86        for (i, cell) in cells.iter().enumerate() {
87            // Skip empty leading/trailing parts
88            if (i == 0 && cell.trim().is_empty() && has_leading_pipe)
89                || (i == cells.len() - 1 && cell.trim().is_empty() && has_trailing_pipe)
90            {
91                continue;
92            }
93            cell_contents.push(cell.trim());
94        }
95
96        // Adjust cell count to match expected count
97        match current_count.cmp(&expected_count) {
98            std::cmp::Ordering::Greater => {
99                // Too many cells, remove excess
100                cell_contents.truncate(expected_count);
101            }
102            std::cmp::Ordering::Less => {
103                // Too few cells, add empty ones
104                while cell_contents.len() < expected_count {
105                    cell_contents.push("");
106                }
107            }
108            std::cmp::Ordering::Equal => {
109                // Perfect number of cells, no adjustment needed
110            }
111        }
112
113        // Reconstruct row
114        let mut result = String::new();
115        if has_leading_pipe {
116            result.push('|');
117        }
118
119        for (i, cell) in cell_contents.iter().enumerate() {
120            result.push_str(&format!(" {cell} "));
121            if i < cell_contents.len() - 1 || has_trailing_pipe {
122                result.push('|');
123            }
124        }
125
126        Some(result)
127    }
128
129    /// Split a table row into cells, respecting flavor-specific behavior
130    ///
131    /// For Standard/GFM flavor, pipes in inline code ARE cell delimiters.
132    /// For MkDocs flavor, pipes in inline code are NOT cell delimiters.
133    fn split_row_into_cells(row: &str, flavor: crate::config::MarkdownFlavor) -> Vec<String> {
134        // First, mask escaped pipes (same for all flavors)
135        let masked = TableUtils::mask_pipes_for_table_parsing(row);
136
137        // For MkDocs flavor, also mask pipes inside inline code
138        let final_masked = if flavor == crate::config::MarkdownFlavor::MkDocs {
139            TableUtils::mask_pipes_in_inline_code(&masked)
140        } else {
141            masked
142        };
143
144        // Split by pipes on the masked string, then extract corresponding
145        // original content from the unmasked row
146        let masked_parts: Vec<&str> = final_masked.split('|').collect();
147        let mut cells = Vec::new();
148        let mut pos = 0;
149
150        for masked_part in masked_parts {
151            let cell_len = masked_part.len();
152            if pos + cell_len <= row.len() {
153                cells.push(row[pos..pos + cell_len].to_string());
154            } else {
155                cells.push(masked_part.to_string());
156            }
157            pos += cell_len + 1; // +1 for the pipe delimiter
158        }
159
160        cells
161    }
162}
163
164impl Rule for MD056TableColumnCount {
165    fn name(&self) -> &'static str {
166        "MD056"
167    }
168
169    fn description(&self) -> &'static str {
170        "Table column count should be consistent"
171    }
172
173    fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
174        // Skip if no tables present
175        !ctx.likely_has_tables()
176    }
177
178    fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
179        let content = ctx.content;
180        let flavor = ctx.flavor;
181        let mut warnings = Vec::new();
182
183        // Early return for empty content or content without tables
184        if content.is_empty() || !content.contains('|') {
185            return Ok(Vec::new());
186        }
187
188        let lines: Vec<&str> = content.lines().collect();
189
190        // Use pre-computed table blocks from context
191        let table_blocks = &ctx.table_blocks;
192
193        for table_block in table_blocks {
194            // Determine expected column count from header row
195            let expected_count = TableUtils::count_cells_with_flavor(lines[table_block.header_line], flavor);
196
197            if expected_count == 0 {
198                continue; // Skip invalid tables
199            }
200
201            // Collect all table lines for building the whole-table fix
202            let all_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
203                .chain(std::iter::once(table_block.delimiter_line))
204                .chain(table_block.content_lines.iter().copied())
205                .collect();
206
207            // Build the whole-table fix once for all warnings in this table
208            // This ensures that applying Quick Fix on any row fixes the entire table
209            let table_start_line = table_block.start_line + 1; // Convert to 1-indexed
210            let table_end_line = table_block.end_line + 1; // Convert to 1-indexed
211
212            // Build the complete fixed table content
213            let mut fixed_table_lines: Vec<String> = Vec::with_capacity(all_line_indices.len());
214            for &line_idx in &all_line_indices {
215                let line = lines[line_idx];
216                let fixed_line = self
217                    .fix_table_row(line, expected_count, flavor)
218                    .unwrap_or_else(|| line.to_string());
219                if line_idx < lines.len() - 1 {
220                    fixed_table_lines.push(format!("{fixed_line}\n"));
221                } else {
222                    fixed_table_lines.push(fixed_line);
223                }
224            }
225            let table_replacement = fixed_table_lines.concat();
226            let table_range = ctx.line_index.multi_line_range(table_start_line, table_end_line);
227
228            // Check all rows in the table
229            for &line_idx in &all_line_indices {
230                let line = lines[line_idx];
231                let count = TableUtils::count_cells_with_flavor(line, flavor);
232
233                if count > 0 && count != expected_count {
234                    // Calculate precise character range for the entire table row
235                    let (start_line, start_col, end_line, end_col) = calculate_line_range(line_idx + 1, line);
236
237                    // Each warning uses the same whole-table fix
238                    // This ensures Quick Fix on any row fixes the entire table
239                    warnings.push(LintWarning {
240                        rule_name: Some(self.name().to_string()),
241                        message: format!("Table row has {count} cells, but expected {expected_count}"),
242                        line: start_line,
243                        column: start_col,
244                        end_line,
245                        end_column: end_col,
246                        severity: Severity::Warning,
247                        fix: Some(Fix {
248                            range: table_range.clone(),
249                            replacement: table_replacement.clone(),
250                        }),
251                    });
252                }
253            }
254        }
255
256        Ok(warnings)
257    }
258
259    fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
260        let content = ctx.content;
261        let flavor = ctx.flavor;
262        let lines: Vec<&str> = content.lines().collect();
263        let table_blocks = &ctx.table_blocks;
264
265        let mut result_lines: Vec<String> = lines.iter().map(|&s| s.to_string()).collect();
266
267        for table_block in table_blocks {
268            // Determine expected column count from header row
269            let expected_count = TableUtils::count_cells_with_flavor(lines[table_block.header_line], flavor);
270
271            if expected_count == 0 {
272                continue; // Skip invalid tables
273            }
274
275            // Fix all rows in the table
276            let all_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
277                .chain(std::iter::once(table_block.delimiter_line))
278                .chain(table_block.content_lines.iter().copied())
279                .collect();
280
281            for &line_idx in &all_line_indices {
282                let line = lines[line_idx];
283                if let Some(fixed_line) = self.fix_table_row(line, expected_count, flavor) {
284                    result_lines[line_idx] = fixed_line;
285                }
286            }
287        }
288
289        let mut fixed = result_lines.join("\n");
290        // Preserve trailing newline if original content had one
291        if content.ends_with('\n') && !fixed.ends_with('\n') {
292            fixed.push('\n');
293        }
294        Ok(fixed)
295    }
296
297    fn as_any(&self) -> &dyn std::any::Any {
298        self
299    }
300
301    fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
302    where
303        Self: Sized,
304    {
305        Box::new(MD056TableColumnCount)
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use crate::lint_context::LintContext;
313
314    #[test]
315    fn test_valid_table() {
316        let rule = MD056TableColumnCount;
317        let content = "| Header 1 | Header 2 | Header 3 |
318|----------|----------|----------|
319| Cell 1   | Cell 2   | Cell 3   |
320| Cell 4   | Cell 5   | Cell 6   |";
321        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
322        let result = rule.check(&ctx).unwrap();
323
324        assert_eq!(result.len(), 0);
325    }
326
327    #[test]
328    fn test_too_few_columns() {
329        let rule = MD056TableColumnCount;
330        let content = "| Header 1 | Header 2 | Header 3 |
331|----------|----------|----------|
332| Cell 1   | Cell 2   |
333| Cell 4   | Cell 5   | Cell 6   |";
334        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
335        let result = rule.check(&ctx).unwrap();
336
337        assert_eq!(result.len(), 1);
338        assert_eq!(result[0].line, 3);
339        assert!(result[0].message.contains("has 2 cells, but expected 3"));
340    }
341
342    #[test]
343    fn test_too_many_columns() {
344        let rule = MD056TableColumnCount;
345        let content = "| Header 1 | Header 2 |
346|----------|----------|
347| Cell 1   | Cell 2   | Cell 3   | Cell 4   |
348| Cell 5   | Cell 6   |";
349        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
350        let result = rule.check(&ctx).unwrap();
351
352        assert_eq!(result.len(), 1);
353        assert_eq!(result[0].line, 3);
354        assert!(result[0].message.contains("has 4 cells, but expected 2"));
355    }
356
357    #[test]
358    fn test_delimiter_row_mismatch() {
359        let rule = MD056TableColumnCount;
360        let content = "| Header 1 | Header 2 | Header 3 |
361|----------|----------|
362| Cell 1   | Cell 2   | Cell 3   |";
363        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
364        let result = rule.check(&ctx).unwrap();
365
366        assert_eq!(result.len(), 1);
367        assert_eq!(result[0].line, 2);
368        assert!(result[0].message.contains("has 2 cells, but expected 3"));
369    }
370
371    #[test]
372    fn test_fix_too_few_columns() {
373        let rule = MD056TableColumnCount;
374        let content = "| Header 1 | Header 2 | Header 3 |
375|----------|----------|----------|
376| Cell 1   | Cell 2   |
377| Cell 4   | Cell 5   | Cell 6   |";
378        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
379        let fixed = rule.fix(&ctx).unwrap();
380
381        assert!(fixed.contains("| Cell 1 | Cell 2 |  |"));
382    }
383
384    #[test]
385    fn test_fix_too_many_columns() {
386        let rule = MD056TableColumnCount;
387        let content = "| Header 1 | Header 2 |
388|----------|----------|
389| Cell 1   | Cell 2   | Cell 3   | Cell 4   |
390| Cell 5   | Cell 6   |";
391        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
392        let fixed = rule.fix(&ctx).unwrap();
393
394        assert!(fixed.contains("| Cell 1 | Cell 2 |"));
395        assert!(!fixed.contains("Cell 3"));
396        assert!(!fixed.contains("Cell 4"));
397    }
398
399    #[test]
400    fn test_no_leading_pipe() {
401        let rule = MD056TableColumnCount;
402        let content = "Header 1 | Header 2 | Header 3 |
403---------|----------|----------|
404Cell 1   | Cell 2   |
405Cell 4   | Cell 5   | Cell 6   |";
406        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
407        let result = rule.check(&ctx).unwrap();
408
409        assert_eq!(result.len(), 1);
410        assert_eq!(result[0].line, 3);
411    }
412
413    #[test]
414    fn test_no_trailing_pipe() {
415        let rule = MD056TableColumnCount;
416        let content = "| Header 1 | Header 2 | Header 3
417|----------|----------|----------
418| Cell 1   | Cell 2
419| Cell 4   | Cell 5   | Cell 6";
420        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
421        let result = rule.check(&ctx).unwrap();
422
423        assert_eq!(result.len(), 1);
424        assert_eq!(result[0].line, 3);
425    }
426
427    #[test]
428    fn test_no_pipes_at_all() {
429        let rule = MD056TableColumnCount;
430        let content = "This is not a table
431Just regular text
432No pipes here";
433        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
434        let result = rule.check(&ctx).unwrap();
435
436        assert_eq!(result.len(), 0);
437    }
438
439    #[test]
440    fn test_empty_cells() {
441        let rule = MD056TableColumnCount;
442        let content = "| Header 1 | Header 2 | Header 3 |
443|----------|----------|----------|
444|          |          |          |
445| Cell 1   |          | Cell 3   |";
446        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
447        let result = rule.check(&ctx).unwrap();
448
449        assert_eq!(result.len(), 0);
450    }
451
452    #[test]
453    fn test_multiple_tables() {
454        let rule = MD056TableColumnCount;
455        let content = "| Table 1 Col 1 | Table 1 Col 2 |
456|----------------|----------------|
457| Data 1         | Data 2         |
458
459Some text in between.
460
461| Table 2 Col 1 | Table 2 Col 2 | Table 2 Col 3 |
462|----------------|----------------|----------------|
463| Data 3         | Data 4         |
464| Data 5         | Data 6         | Data 7         |";
465        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
466        let result = rule.check(&ctx).unwrap();
467
468        assert_eq!(result.len(), 1);
469        assert_eq!(result[0].line, 9);
470        assert!(result[0].message.contains("has 2 cells, but expected 3"));
471    }
472
473    #[test]
474    fn test_table_with_escaped_pipes() {
475        let rule = MD056TableColumnCount;
476
477        // Single backslash escapes the pipe: \| keeps pipe as content (2 columns)
478        let content = "| Command | Description |
479|---------|-------------|
480| `echo \\| grep` | Pipe example |
481| `ls` | List files |";
482        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
483        let result = rule.check(&ctx).unwrap();
484        assert_eq!(result.len(), 0, "escaped pipe \\| should not split cells");
485
486        // Double backslash + pipe: \\| means escaped backslash + pipe delimiter (3 columns)
487        let content_double = "| Command | Description |
488|---------|-------------|
489| `echo \\\\| grep` | Pipe example |
490| `ls` | List files |";
491        let ctx2 = LintContext::new(content_double, crate::config::MarkdownFlavor::Standard, None);
492        let result2 = rule.check(&ctx2).unwrap();
493        // Line 3 has \\| which becomes 3 cells, but header expects 2
494        assert_eq!(result2.len(), 1, "double backslash \\\\| should split cells");
495    }
496
497    #[test]
498    fn test_empty_content() {
499        let rule = MD056TableColumnCount;
500        let content = "";
501        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
502        let result = rule.check(&ctx).unwrap();
503
504        assert_eq!(result.len(), 0);
505    }
506
507    #[test]
508    fn test_code_block_with_table() {
509        let rule = MD056TableColumnCount;
510        let content = "```
511| This | Is | Code |
512|------|----|----|
513| Not  | A  | Table |
514```
515
516| Real | Table |
517|------|-------|
518| Data | Here  |";
519        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
520        let result = rule.check(&ctx).unwrap();
521
522        // Should not check tables inside code blocks
523        assert_eq!(result.len(), 0);
524    }
525
526    #[test]
527    fn test_fix_preserves_pipe_style() {
528        let rule = MD056TableColumnCount;
529        // Test with no trailing pipes
530        let content = "| Header 1 | Header 2 | Header 3
531|----------|----------|----------
532| Cell 1   | Cell 2";
533        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
534        let fixed = rule.fix(&ctx).unwrap();
535
536        let lines: Vec<&str> = fixed.lines().collect();
537        assert!(!lines[2].ends_with('|'));
538        assert!(lines[2].contains("Cell 1"));
539        assert!(lines[2].contains("Cell 2"));
540    }
541
542    #[test]
543    fn test_single_column_table() {
544        let rule = MD056TableColumnCount;
545        let content = "| Header |
546|---------|
547| Cell 1  |
548| Cell 2  |";
549        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
550        let result = rule.check(&ctx).unwrap();
551
552        assert_eq!(result.len(), 0);
553    }
554
555    #[test]
556    fn test_complex_delimiter_row() {
557        let rule = MD056TableColumnCount;
558        let content = "| Left | Center | Right |
559|:-----|:------:|------:|
560| L    | C      | R     |
561| Left | Center |";
562        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
563        let result = rule.check(&ctx).unwrap();
564
565        assert_eq!(result.len(), 1);
566        assert_eq!(result[0].line, 4);
567    }
568
569    #[test]
570    fn test_unicode_content() {
571        let rule = MD056TableColumnCount;
572        let content = "| 名前 | 年齢 | 都市 |
573|------|------|------|
574| 田中 | 25   | 東京 |
575| 佐藤 | 30   |";
576        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
577        let result = rule.check(&ctx).unwrap();
578
579        assert_eq!(result.len(), 1);
580        assert_eq!(result[0].line, 4);
581    }
582
583    #[test]
584    fn test_very_long_cells() {
585        let rule = MD056TableColumnCount;
586        let content = "| Short | Very very very very very very very very very very long header | Another |
587|-------|--------------------------------------------------------------|---------|
588| Data  | This is an extremely long cell content that goes on and on   |";
589        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
590        let result = rule.check(&ctx).unwrap();
591
592        assert_eq!(result.len(), 1);
593        assert!(result[0].message.contains("has 2 cells, but expected 3"));
594    }
595
596    #[test]
597    fn test_fix_with_newline_ending() {
598        let rule = MD056TableColumnCount;
599        let content = "| A | B | C |
600|---|---|---|
601| 1 | 2 |
602";
603        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
604        let fixed = rule.fix(&ctx).unwrap();
605
606        assert!(fixed.ends_with('\n'));
607        assert!(fixed.contains("| 1 | 2 |  |"));
608    }
609
610    #[test]
611    fn test_fix_without_newline_ending() {
612        let rule = MD056TableColumnCount;
613        let content = "| A | B | C |
614|---|---|---|
615| 1 | 2 |";
616        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
617        let fixed = rule.fix(&ctx).unwrap();
618
619        assert!(!fixed.ends_with('\n'));
620        assert!(fixed.contains("| 1 | 2 |  |"));
621    }
622
623    #[test]
624    fn test_blockquote_table_column_mismatch() {
625        let rule = MD056TableColumnCount;
626        let content = "> | Header 1 | Header 2 | Header 3 |
627> |----------|----------|----------|
628> | Cell 1   | Cell 2   |";
629        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
630        let result = rule.check(&ctx).unwrap();
631
632        assert_eq!(result.len(), 1);
633        assert_eq!(result[0].line, 3);
634        assert!(result[0].message.contains("has 2 cells, but expected 3"));
635    }
636
637    #[test]
638    fn test_fix_blockquote_table_preserves_prefix() {
639        let rule = MD056TableColumnCount;
640        let content = "> | Header 1 | Header 2 | Header 3 |
641> |----------|----------|----------|
642> | Cell 1   | Cell 2   |
643> | Cell 4   | Cell 5   | Cell 6   |";
644        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
645        let fixed = rule.fix(&ctx).unwrap();
646
647        // Each line should still start with "> "
648        for line in fixed.lines() {
649            assert!(line.starts_with("> "), "Line should preserve blockquote prefix: {line}");
650        }
651        // The fixed row should have 3 cells
652        assert!(fixed.contains("> | Cell 1 | Cell 2 |  |"));
653    }
654
655    #[test]
656    fn test_fix_nested_blockquote_table() {
657        let rule = MD056TableColumnCount;
658        let content = ">> | A | B | C |
659>> |---|---|---|
660>> | 1 | 2 |";
661        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
662        let fixed = rule.fix(&ctx).unwrap();
663
664        // Each line should preserve the nested blockquote prefix
665        for line in fixed.lines() {
666            assert!(
667                line.starts_with(">> "),
668                "Line should preserve nested blockquote prefix: {line}"
669            );
670        }
671        assert!(fixed.contains(">> | 1 | 2 |  |"));
672    }
673
674    #[test]
675    fn test_blockquote_table_too_many_columns() {
676        let rule = MD056TableColumnCount;
677        let content = "> | A | B |
678> |---|---|
679> | 1 | 2 | 3 | 4 |";
680        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
681        let fixed = rule.fix(&ctx).unwrap();
682
683        // Should preserve blockquote prefix while truncating columns
684        assert!(fixed.lines().nth(2).unwrap().starts_with("> "));
685        assert!(fixed.contains("> | 1 | 2 |"));
686        assert!(!fixed.contains("| 3 |"));
687    }
688}