1use crate::rule::{Fix, FixCapability, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::utils::range_utils::calculate_line_range;
3use crate::utils::table_utils::TableUtils;
4
5#[derive(Debug, Clone)]
10pub struct MD056TableColumnCount;
11
12impl Default for MD056TableColumnCount {
13 fn default() -> Self {
14 MD056TableColumnCount
15 }
16}
17
18impl MD056TableColumnCount {
19 fn fix_table_row_content(
21 &self,
22 expected_count: usize,
23 flavor: crate::config::MarkdownFlavor,
24 table_block: &crate::utils::table_utils::TableBlock,
25 line_index: usize,
26 original_line: &str,
27 header: &str,
28 ) -> Option<String> {
29 let row_content = TableUtils::extract_table_row_content(original_line, table_block, line_index);
30 let current_count = TableUtils::count_cells_with_flavor(row_content, flavor);
31
32 if current_count == expected_count || current_count == 0 {
33 return None;
34 }
35
36 let is_delimiter = line_index == 1;
37 let fixed = self.fix_row_cell_count(row_content, expected_count, is_delimiter, flavor)?;
38 Some(self.restore_prefixes(&fixed, table_block, line_index, original_line, header))
39 }
40
41 fn restore_prefixes(
43 &self,
44 fixed_content: &str,
45 table_block: &crate::utils::table_utils::TableBlock,
46 line_index: usize,
47 original_line: &str,
48 header: &str,
49 ) -> String {
50 let (blockquote_prefix, _) = TableUtils::extract_blockquote_prefix(original_line);
52
53 if let Some(ref list_ctx) = table_block.list_context {
55 if line_index == 0 {
56 format!("{blockquote_prefix}{}{fixed_content}", list_ctx.list_prefix)
58 } else {
59 let indent = " ".repeat(list_ctx.content_indent);
61 format!("{blockquote_prefix}{indent}{fixed_content}")
62 }
63 } else {
64 let prefix = TableUtils::row_prefix(original_line, header, table_block, line_index);
66 format!("{prefix}{fixed_content}")
67 }
68 }
69
70 fn fix_row_cell_count(
80 &self,
81 row: &str,
82 expected_count: usize,
83 is_delimiter: bool,
84 flavor: crate::config::MarkdownFlavor,
85 ) -> Option<String> {
86 let current_count = TableUtils::count_cells_with_flavor(row, flavor);
87
88 if current_count == expected_count || current_count == 0 {
89 return None;
90 }
91
92 let trimmed = row.trim();
93 let has_leading_pipe = trimmed.starts_with('|');
94 let has_trailing_pipe = trimmed.ends_with('|');
95
96 let cells = TableUtils::split_table_row_with_flavor(trimmed, flavor);
98 let mut cell_contents: Vec<&str> = cells.iter().map(|c| c.trim()).collect();
99
100 match current_count.cmp(&expected_count) {
101 std::cmp::Ordering::Greater => {
102 let extra = cell_contents.get(expected_count..).unwrap_or_default();
103 if !is_delimiter && extra.iter().any(|cell| !cell.is_empty()) {
104 return None;
105 }
106 cell_contents.truncate(expected_count);
107 }
108 std::cmp::Ordering::Less => {
109 let padding = if is_delimiter { "---" } else { "" };
110 cell_contents.resize(expected_count, padding);
111 }
112 std::cmp::Ordering::Equal => {}
113 }
114
115 let close_row = has_trailing_pipe || cell_contents.last().is_some_and(|cell| cell.is_empty());
118
119 let mut result = String::new();
121 if has_leading_pipe {
122 result.push('|');
123 }
124
125 for (i, cell) in cell_contents.iter().enumerate() {
126 result.push_str(&format!(" {cell} "));
127 if i < cell_contents.len() - 1 || close_row {
128 result.push('|');
129 }
130 }
131
132 Some(result)
133 }
134}
135
136impl Rule for MD056TableColumnCount {
137 fn native_format_policy(&self) -> Option<crate::format_proof::NativeFormatPolicy> {
138 Some(crate::format_proof::NativeFormatPolicy::for_rule::<Self>(56))
139 }
140
141 fn name(&self) -> &'static str {
142 "MD056"
143 }
144
145 fn description(&self) -> &'static str {
146 "Table column count should be consistent"
147 }
148
149 fn category(&self) -> RuleCategory {
150 RuleCategory::Table
151 }
152
153 fn fix_capability(&self) -> FixCapability {
154 FixCapability::ConditionallyFixable
155 }
156
157 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
158 !ctx.likely_has_tables()
160 }
161
162 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
163 let content = ctx.content;
164 let flavor = ctx.flavor;
165 let mut warnings = Vec::new();
166
167 if content.is_empty() || !content.contains('|') {
169 return Ok(Vec::new());
170 }
171
172 let lines = ctx.raw_lines();
173
174 let table_blocks = &ctx.table_blocks;
176
177 for table_block in table_blocks
178 .iter()
179 .filter(|block| block.parser_confirmed || block.repairable)
180 {
181 let all_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
183 .chain(std::iter::once(table_block.delimiter_line))
184 .chain(table_block.content_lines.iter().copied())
185 .collect();
186
187 let header_content = TableUtils::extract_table_row_content(lines[table_block.header_line], table_block, 0);
189 let expected_count = TableUtils::count_cells_with_flavor(header_content, flavor);
190
191 if expected_count == 0 {
192 continue; }
194
195 for (i, &line_idx) in all_line_indices.iter().enumerate() {
199 let line = lines[line_idx];
200 let row_content = TableUtils::extract_table_row_content(line, table_block, i);
201 let count = TableUtils::count_cells_with_flavor(row_content, flavor);
202
203 if count > 0 && count != expected_count {
204 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_idx + 1, line);
205
206 let fix = self
210 .fix_table_row_content(
211 expected_count,
212 flavor,
213 table_block,
214 i,
215 line,
216 lines[table_block.header_line],
217 )
218 .map(|fixed_line| {
219 let row_range =
220 ctx.line_column_byte_range_with_length(line_idx + 1, 1, line.chars().count());
221 Fix::new(row_range, fixed_line)
222 });
223
224 warnings.push(LintWarning {
225 rule_name: Some(self.name().to_string()),
226 message: format!("Table row has {count} cells, but expected {expected_count}"),
227 line: start_line,
228 column: start_col,
229 end_line,
230 end_column: end_col,
231 severity: Severity::Warning,
232 fix,
233 });
234 }
235 }
236 }
237
238 Ok(warnings)
239 }
240
241 fn fixes_from_diagnostics(&self) -> bool {
242 true
243 }
244
245 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
246 if self.should_skip(ctx) {
247 return Ok(ctx.content.to_string());
248 }
249 let warnings = self.check(ctx)?;
250 if warnings.is_empty() {
251 return Ok(ctx.content.to_string());
252 }
253 let warnings =
254 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
255 crate::utils::fix_utils::apply_warning_fixes(ctx.content, &warnings).map_err(LintError::InvalidInput)
256 }
257
258 fn as_any(&self) -> &dyn std::any::Any {
259 self
260 }
261
262 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
263 where
264 Self: Sized,
265 {
266 Box::new(MD056TableColumnCount)
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use crate::lint_context::LintContext;
274
275 #[test]
276 fn test_valid_table() {
277 let rule = MD056TableColumnCount;
278 let content = "| Header 1 | Header 2 | Header 3 |
279|----------|----------|----------|
280| Cell 1 | Cell 2 | Cell 3 |
281| Cell 4 | Cell 5 | Cell 6 |";
282 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
283 let result = rule.check(&ctx).unwrap();
284
285 assert_eq!(result.len(), 0);
286 }
287
288 #[test]
289 fn test_too_few_columns() {
290 let rule = MD056TableColumnCount;
291 let content = "| Header 1 | Header 2 | Header 3 |
292|----------|----------|----------|
293| Cell 1 | Cell 2 |
294| Cell 4 | Cell 5 | Cell 6 |";
295 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
296 let result = rule.check(&ctx).unwrap();
297
298 assert_eq!(result.len(), 1);
299 assert_eq!(result[0].line, 3);
300 assert!(result[0].message.contains("has 2 cells, but expected 3"));
301 }
302
303 #[test]
304 fn test_too_many_columns() {
305 let rule = MD056TableColumnCount;
306 let content = "| Header 1 | Header 2 |
307|----------|----------|
308| Cell 1 | Cell 2 | Cell 3 | Cell 4 |
309| Cell 5 | Cell 6 |";
310 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
311 let result = rule.check(&ctx).unwrap();
312
313 assert_eq!(result.len(), 1);
314 assert_eq!(result[0].line, 3);
315 assert!(result[0].message.contains("has 4 cells, but expected 2"));
316 }
317
318 #[test]
319 fn test_delimiter_row_mismatch() {
320 let rule = MD056TableColumnCount;
321 let content = "| Header 1 | Header 2 | Header 3 |
322|----------|----------|
323| Cell 1 | Cell 2 | Cell 3 |";
324 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
325 let result = rule.check(&ctx).unwrap();
326
327 assert_eq!(result.len(), 1);
328 assert_eq!(result[0].line, 2);
329 assert!(result[0].message.contains("has 2 cells, but expected 3"));
330 }
331
332 #[test]
333 fn test_fix_too_few_columns() {
334 let rule = MD056TableColumnCount;
335 let content = "| Header 1 | Header 2 | Header 3 |
336|----------|----------|----------|
337| Cell 1 | Cell 2 |
338| Cell 4 | Cell 5 | Cell 6 |";
339 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
340 let fixed = rule.fix(&ctx).unwrap();
341
342 assert!(fixed.contains("| Cell 1 | Cell 2 | |"));
343 }
344
345 #[test]
349 fn test_fix_keeps_extra_cells_with_content() {
350 let rule = MD056TableColumnCount;
351 let content = "| Header 1 | Header 2 |
352|----------|----------|
353| Cell 1 | Cell 2 | Cell 3 | Cell 4 |
354| Cell 5 | Cell 6 |";
355 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
356 let warnings = rule.check(&ctx).unwrap();
357 assert_eq!(warnings.len(), 1);
358 assert!(
359 warnings[0].fix.is_none(),
360 "no fix may be offered: {:?}",
361 warnings[0].fix
362 );
363
364 assert_eq!(rule.fix(&ctx).unwrap(), content);
365 }
366
367 #[test]
368 fn test_fix_keeps_extra_cell_from_issue_940() {
369 let rule = MD056TableColumnCount;
370 let content = "# T\n\n| A | B | C |\n| - | - | - |\n| one | two | three | four |\n";
371 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
372 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
373 assert_eq!(rule.fix(&ctx).unwrap(), content);
374 }
375
376 #[test]
379 fn test_fix_keeps_row_when_any_extra_cell_has_content() {
380 let rule = MD056TableColumnCount;
381 for content in [
382 "| A | B |\n|---|---|\n| 1 | 2 | | x |\n",
383 "| A | B |\n|---|---|\n| 1 | 2 | x | |\n",
384 "| A | B |\n|---|---|\n| 1 | 2 | `x` |\n",
385 ] {
386 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
387 let warnings = rule.check(&ctx).unwrap();
388 assert_eq!(warnings.len(), 1, "{content:?}");
389 assert!(warnings[0].fix.is_none(), "{content:?}");
390 assert_eq!(rule.fix(&ctx).unwrap(), content);
391 }
392 }
393
394 #[test]
396 fn test_fix_removes_empty_extra_cells() {
397 let rule = MD056TableColumnCount;
398 for (content, expected) in [
399 (
400 "| A | B |\n|---|---|\n| 1 | 2 | |\n",
401 "| A | B |\n|---|---|\n| 1 | 2 |\n",
402 ),
403 (
404 "| A | B |\n|---|---|\n| 1 | 2 | | |\n",
405 "| A | B |\n|---|---|\n| 1 | 2 |\n",
406 ),
407 ] {
408 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
409 assert_eq!(rule.fix(&ctx).unwrap(), expected, "{content:?}");
410 }
411 }
412
413 #[test]
417 fn test_fix_delimiter_row() {
418 let rule = MD056TableColumnCount;
419 for (content, expected) in [
420 (
421 "| A | B | C |\n|---|---|\n| 1 | 2 | 3 |\n",
422 "| A | B | C |\n| --- | --- | --- |\n| 1 | 2 | 3 |\n",
423 ),
424 (
425 "| A | B |\n|:--|--:|---|\n| 1 | 2 |\n",
426 "| A | B |\n| :-- | --: |\n| 1 | 2 |\n",
427 ),
428 ] {
429 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
430 let fixed = rule.fix(&ctx).unwrap();
431 assert_eq!(fixed, expected, "{content:?}");
432 let ctx = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
433 assert!(rule.check(&ctx).unwrap().is_empty(), "{fixed:?}");
434 }
435 }
436
437 #[test]
438 fn test_no_leading_pipe() {
439 let rule = MD056TableColumnCount;
440 let content = "Header 1 | Header 2 | Header 3 |
441---------|----------|----------|
442Cell 1 | Cell 2 |
443Cell 4 | Cell 5 | Cell 6 |";
444 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
445 let result = rule.check(&ctx).unwrap();
446
447 assert_eq!(result.len(), 1);
448 assert_eq!(result[0].line, 3);
449 }
450
451 #[test]
452 fn test_no_trailing_pipe() {
453 let rule = MD056TableColumnCount;
454 let content = "| Header 1 | Header 2 | Header 3
455|----------|----------|----------
456| Cell 1 | Cell 2
457| Cell 4 | Cell 5 | Cell 6";
458 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
459 let result = rule.check(&ctx).unwrap();
460
461 assert_eq!(result.len(), 1);
462 assert_eq!(result[0].line, 3);
463 }
464
465 #[test]
466 fn test_no_pipes_at_all() {
467 let rule = MD056TableColumnCount;
468 let content = "This is not a table
469Just regular text
470No pipes here";
471 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
472 let result = rule.check(&ctx).unwrap();
473
474 assert_eq!(result.len(), 0);
475 }
476
477 #[test]
478 fn test_empty_cells() {
479 let rule = MD056TableColumnCount;
480 let content = "| Header 1 | Header 2 | Header 3 |
481|----------|----------|----------|
482| | | |
483| Cell 1 | | Cell 3 |";
484 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
485 let result = rule.check(&ctx).unwrap();
486
487 assert_eq!(result.len(), 0);
488 }
489
490 #[test]
491 fn test_multiple_tables() {
492 let rule = MD056TableColumnCount;
493 let content = "| Table 1 Col 1 | Table 1 Col 2 |
494|----------------|----------------|
495| Data 1 | Data 2 |
496
497Some text in between.
498
499| Table 2 Col 1 | Table 2 Col 2 | Table 2 Col 3 |
500|----------------|----------------|----------------|
501| Data 3 | Data 4 |
502| Data 5 | Data 6 | Data 7 |";
503 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
504 let result = rule.check(&ctx).unwrap();
505
506 assert_eq!(result.len(), 1);
507 assert_eq!(result[0].line, 9);
508 assert!(result[0].message.contains("has 2 cells, but expected 3"));
509 }
510
511 #[test]
512 fn test_table_with_escaped_pipes() {
513 let rule = MD056TableColumnCount;
514
515 let content = "| Command | Description |
517|---------|-------------|
518| `echo \\| grep` | Pipe example |
519| `ls` | List files |";
520 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
521 let result = rule.check(&ctx).unwrap();
522 assert_eq!(result.len(), 0, "escaped pipe \\| should not split cells");
523
524 let content_double = "| Command | Description |
526|---------|-------------|
527| `echo \\\\| grep` | Pipe example |
528| `ls` | List files |";
529 let ctx2 = LintContext::new(content_double, crate::config::MarkdownFlavor::Standard, None);
530 let result2 = rule.check(&ctx2).unwrap();
531 assert_eq!(result2.len(), 0, "pipes inside code spans should not split cells");
533 }
534
535 #[test]
539 fn padding_a_row_without_a_closing_pipe_closes_it() {
540 let rule = MD056TableColumnCount;
541 for (content, expected) in [
542 ("a | b | c\n--|--|--\nx | y\n", "a | b | c\n--|--|--\n x | y | |\n"),
543 ("| | |\n| --- | --- |\n`|`\n", "| | |\n| --- | --- |\n `|` | |\n"),
544 (
545 "| a | b | c |\n|--|--|--|\n| x | y |\n",
546 "| a | b | c |\n|--|--|--|\n| x | y | |\n",
547 ),
548 ] {
549 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
550 let fixed = rule.fix(&ctx).unwrap();
551 assert_eq!(fixed, expected, "{content:?}");
552 let ctx = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
553 assert_eq!(rule.fix(&ctx).unwrap(), fixed, "second pass over {fixed:?}");
554 }
555 }
556
557 #[test]
558 fn test_empty_content() {
559 let rule = MD056TableColumnCount;
560 let content = "";
561 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
562 let result = rule.check(&ctx).unwrap();
563
564 assert_eq!(result.len(), 0);
565 }
566
567 #[test]
568 fn test_code_block_with_table() {
569 let rule = MD056TableColumnCount;
570 let content = "```
571| This | Is | Code |
572|------|----|----|
573| Not | A | Table |
574```
575
576| Real | Table |
577|------|-------|
578| Data | Here |";
579 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
580 let result = rule.check(&ctx).unwrap();
581
582 assert_eq!(result.len(), 0);
584 }
585
586 #[test]
587 fn test_fix_preserves_pipe_style() {
588 let rule = MD056TableColumnCount;
589 let content = "| Header 1 | Header 2
592|----------|----------|---
593| Cell 1 | Cell 2 | |";
594 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
595 let fixed = rule.fix(&ctx).unwrap();
596
597 let lines: Vec<&str> = fixed.lines().collect();
598 assert_eq!(lines[1], "| ---------- | ---------- ");
599 assert_eq!(lines[2], "| Cell 1 | Cell 2 |");
600 }
601
602 #[test]
604 fn test_fix_keeps_open_row_with_extra_content() {
605 let rule = MD056TableColumnCount;
606 let content = "| Header 1 | Header 2
607|----------|----------
608| Cell 1 | Cell 2 | Cell 3";
609 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
610 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
611 assert_eq!(rule.fix(&ctx).unwrap(), content);
612 }
613
614 #[test]
615 fn test_single_column_table() {
616 let rule = MD056TableColumnCount;
617 let content = "| Header |
618|---------|
619| Cell 1 |
620| Cell 2 |";
621 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
622 let result = rule.check(&ctx).unwrap();
623
624 assert_eq!(result.len(), 0);
625 }
626
627 #[test]
628 fn test_complex_delimiter_row() {
629 let rule = MD056TableColumnCount;
630 let content = "| Left | Center | Right |
631|:-----|:------:|------:|
632| L | C | R |
633| Left | Center |";
634 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
635 let result = rule.check(&ctx).unwrap();
636
637 assert_eq!(result.len(), 1);
638 assert_eq!(result[0].line, 4);
639 }
640
641 #[test]
642 fn test_unicode_content() {
643 let rule = MD056TableColumnCount;
644 let content = "| 名前 | 年齢 | 都市 |
645|------|------|------|
646| 田中 | 25 | 東京 |
647| 佐藤 | 30 |";
648 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
649 let result = rule.check(&ctx).unwrap();
650
651 assert_eq!(result.len(), 1);
652 assert_eq!(result[0].line, 4);
653 }
654
655 #[test]
656 fn test_very_long_cells() {
657 let rule = MD056TableColumnCount;
658 let content = "| Short | Very very very very very very very very very very long header | Another |
659|-------|--------------------------------------------------------------|---------|
660| Data | This is an extremely long cell content that goes on and on |";
661 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
662 let result = rule.check(&ctx).unwrap();
663
664 assert_eq!(result.len(), 1);
665 assert!(result[0].message.contains("has 2 cells, but expected 3"));
666 }
667
668 #[test]
669 fn test_fix_with_newline_ending() {
670 let rule = MD056TableColumnCount;
671 let content = "| A | B | C |
672|---|---|---|
673| 1 | 2 |
674";
675 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
676 let fixed = rule.fix(&ctx).unwrap();
677
678 assert!(fixed.ends_with('\n'));
679 assert!(fixed.contains("| 1 | 2 | |"));
680 }
681
682 #[test]
683 fn test_fix_without_newline_ending() {
684 let rule = MD056TableColumnCount;
685 let content = "| A | B | C |
686|---|---|---|
687| 1 | 2 |";
688 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
689 let fixed = rule.fix(&ctx).unwrap();
690
691 assert!(!fixed.ends_with('\n'));
692 assert!(fixed.contains("| 1 | 2 | |"));
693 }
694
695 #[test]
696 fn test_blockquote_table_column_mismatch() {
697 let rule = MD056TableColumnCount;
698 let content = "> | Header 1 | Header 2 | Header 3 |
699> |----------|----------|----------|
700> | Cell 1 | Cell 2 |";
701 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
702 let result = rule.check(&ctx).unwrap();
703
704 assert_eq!(result.len(), 1);
705 assert_eq!(result[0].line, 3);
706 assert!(result[0].message.contains("has 2 cells, but expected 3"));
707 }
708
709 #[test]
710 fn test_fix_blockquote_table_preserves_prefix() {
711 let rule = MD056TableColumnCount;
712 let content = "> | Header 1 | Header 2 | Header 3 |
713> |----------|----------|----------|
714> | Cell 1 | Cell 2 |
715> | Cell 4 | Cell 5 | Cell 6 |";
716 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
717 let fixed = rule.fix(&ctx).unwrap();
718
719 for line in fixed.lines() {
721 assert!(line.starts_with("> "), "Line should preserve blockquote prefix: {line}");
722 }
723 assert!(fixed.contains("> | Cell 1 | Cell 2 | |"));
725 }
726
727 #[test]
728 fn test_fix_nested_blockquote_table() {
729 let rule = MD056TableColumnCount;
730 let content = ">> | A | B | C |
731>> |---|---|---|
732>> | 1 | 2 |";
733 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
734 let fixed = rule.fix(&ctx).unwrap();
735
736 for line in fixed.lines() {
738 assert!(
739 line.starts_with(">> "),
740 "Line should preserve nested blockquote prefix: {line}"
741 );
742 }
743 assert!(fixed.contains(">> | 1 | 2 | |"));
744 }
745
746 #[test]
747 fn test_blockquote_table_too_many_columns() {
748 let rule = MD056TableColumnCount;
749 let content = "> | A | B |
750> |---|---|
751> | 1 | 2 | 3 | 4 |
752> | 5 | 6 | |";
753 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
754 let fixed = rule.fix(&ctx).unwrap();
755
756 assert_eq!(fixed, "> | A | B |\n> |---|---|\n> | 1 | 2 | 3 | 4 |\n> | 5 | 6 |");
759 }
760
761 #[test]
762 fn test_list_table_keeps_extra_cells_with_content() {
763 let rule = MD056TableColumnCount;
764 let content = "- item\n\n | A | B |\n |---|---|\n | 1 | 2 | 3 |\n";
765 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
766 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
767 assert_eq!(rule.fix(&ctx).unwrap(), content);
768 }
769
770 #[test]
771 fn test_fix_capability() {
772 assert_eq!(
773 MD056TableColumnCount.fix_capability(),
774 FixCapability::ConditionallyFixable
775 );
776 }
777
778 fn assert_fix_roundtrip(content: &str) {
781 let rule = MD056TableColumnCount;
782 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
783 let fixed = rule.fix(&ctx).unwrap();
784 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
785 let remaining = rule.check(&ctx2).unwrap();
786 assert!(
787 remaining.is_empty(),
788 "After fix(), check() should find 0 violations.\nOriginal: {content:?}\nFixed: {fixed:?}\nRemaining: {remaining:?}"
789 );
790 }
791
792 #[test]
793 fn test_roundtrip_too_few_columns() {
794 assert_fix_roundtrip("| A | B | C |\n|---|---|---|\n| 1 | 2 |");
795 }
796
797 #[test]
798 fn test_roundtrip_empty_extra_columns() {
799 assert_fix_roundtrip("| A | B |\n|---|---|\n| 1 | 2 | | |");
800 }
801 #[test]
802 fn test_roundtrip_with_trailing_newline() {
803 assert_fix_roundtrip("| A | B | C |\n|---|---|---|\n| 1 | 2 |\n");
804 }
805
806 #[test]
807 fn test_roundtrip_blockquote_table() {
808 assert_fix_roundtrip("> | A | B | C |\n> |---|---|---|\n> | 1 | 2 |");
809 }
810
811 #[test]
812 fn test_roundtrip_clean_table() {
813 assert_fix_roundtrip("| A | B |\n|---|---|\n| 1 | 2 |");
814 }
815
816 #[test]
817 fn test_roundtrip_multiple_tables() {
818 assert_fix_roundtrip("| A | B |\n|---|---|\n| 1 | 2 |\n\nText\n\n| C | D | E |\n|---|---|---|\n| 3 | 4 |");
819 }
820
821 #[test]
837 fn md056_pandoc_grid_tables_not_flagged() {
838 let rule = MD056TableColumnCount;
839 let content = "\
840+---+---+
841| a | b |
842+===+===+
843| 1 | 2 |
844+---+---+
845";
846 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
849 let result = rule.check(&ctx).unwrap();
850 assert!(
851 result.is_empty(),
852 "MD056 should not flag Pandoc grid tables (excluded by table_blocks): {result:?}"
853 );
854
855 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
857 let result_std = rule.check(&ctx_std).unwrap();
858 assert!(
859 result_std.is_empty(),
860 "MD056 should not flag grid-table-like content under Standard either: {result_std:?}"
861 );
862 }
863
864 #[test]
865 fn md056_pandoc_multi_line_tables_not_flagged() {
866 let rule = MD056TableColumnCount;
867 let content = "\
868--------- ----------- ------
869Header 1 Header 2 Header 3
870--------- ----------- ------
871Cell 1 Cell 2 Cell 3
872--------- ----------- ------
873";
874 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
875 let result = rule.check(&ctx).unwrap();
876 assert!(
877 result.is_empty(),
878 "MD056 should not flag Pandoc multi-line tables: {result:?}"
879 );
880
881 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
882 let result_std = rule.check(&ctx_std).unwrap();
883 assert!(
884 result_std.is_empty(),
885 "MD056 should not flag multi-line table content under Standard: {result_std:?}"
886 );
887 }
888
889 #[test]
890 fn md056_pandoc_line_blocks_not_flagged() {
891 let rule = MD056TableColumnCount;
892 let content = "| First line\n| Second line\n";
895 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
896 let result = rule.check(&ctx).unwrap();
897 assert!(
898 result.is_empty(),
899 "MD056 should not treat Pandoc line blocks as tables: {result:?}"
900 );
901
902 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
903 let result_std = rule.check(&ctx_std).unwrap();
904 assert!(
905 result_std.is_empty(),
906 "MD056 should not treat line-block-like content as tables under Standard: {result_std:?}"
907 );
908 }
909
910 #[test]
911 fn md056_pandoc_pipe_table_captions_not_flagged() {
912 let rule = MD056TableColumnCount;
913 let content = "\
915| H1 | H2 |
916|----|-----|
917| a | b |
918
919: My table caption
920";
921 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
922 let result = rule.check(&ctx).unwrap();
923 assert!(
924 result.is_empty(),
925 "MD056 should not flag the pipe-table caption line: {result:?}"
926 );
927
928 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
930 let result_std = rule.check(&ctx_std).unwrap();
931 assert!(
932 result_std.is_empty(),
933 "MD056 already-valid table with caption should have no warnings under Standard: {result_std:?}"
934 );
935 }
936}