1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, Severity};
2use crate::rule_config_serde::RuleConfig;
3use crate::utils::kramdown_utils::is_kramdown_block_attribute;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[serde(rename_all = "kebab-case")]
15pub struct MD058Config {
16 #[serde(default = "default_minimum_before")]
18 pub minimum_before: usize,
19 #[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 pub fn from_config_struct(config: MD058Config) -> Self {
53 Self { config }
54 }
55
56 fn is_blank_line(&self, line: &str) -> bool {
58 line.trim().is_empty()
59 }
60
61 fn count_blank_lines_before(&self, lines: &[&str], line_index: usize) -> usize {
63 let mut count = 0;
64 let mut i = line_index;
65 while i > 0 {
66 i -= 1;
67 if self.is_blank_line(lines[i]) {
68 count += 1;
69 } else {
70 break;
71 }
72 }
73 count
74 }
75
76 fn count_blank_lines_after(&self, lines: &[&str], line_index: usize) -> usize {
78 let mut count = 0;
79 let mut i = line_index + 1;
80 while i < lines.len() {
81 if self.is_blank_line(lines[i]) {
82 count += 1;
83 i += 1;
84 } else {
85 break;
86 }
87 }
88 count
89 }
90}
91
92impl Rule for MD058BlanksAroundTables {
93 fn name(&self) -> &'static str {
94 "MD058"
95 }
96
97 fn description(&self) -> &'static str {
98 "Tables should be surrounded by blank lines"
99 }
100
101 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
102 !ctx.likely_has_tables()
104 }
105
106 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
107 let content = ctx.content;
108 let _line_index = &ctx.line_index;
109 let mut warnings = Vec::new();
110
111 if content.is_empty() || !content.contains('|') {
113 return Ok(Vec::new());
114 }
115
116 let lines: Vec<&str> = content.lines().collect();
117
118 let table_blocks = &ctx.table_blocks;
120
121 for table_block in table_blocks {
122 if table_block.start_line > 0 {
124 let blank_lines_before = self.count_blank_lines_before(&lines, table_block.start_line);
125 if blank_lines_before < self.config.minimum_before {
126 let needed = self.config.minimum_before - blank_lines_before;
127 let message = if self.config.minimum_before == 1 {
128 "Missing blank line before table".to_string()
129 } else {
130 format!("Missing {needed} blank lines before table")
131 };
132
133 warnings.push(LintWarning {
134 rule_name: Some(self.name().to_string()),
135 message,
136 line: table_block.start_line + 1,
137 column: 1,
138 end_line: table_block.start_line + 1,
139 end_column: 2,
140 severity: Severity::Warning,
141 fix: Some(Fix {
142 range: _line_index.line_col_to_byte_range(table_block.start_line + 1, 1),
144 replacement: "\n".repeat(needed),
145 }),
146 });
147 }
148 }
149
150 if table_block.end_line < lines.len() - 1 {
152 let next_line_is_attribute = if table_block.end_line + 1 < lines.len() {
154 is_kramdown_block_attribute(lines[table_block.end_line + 1])
155 } else {
156 false
157 };
158
159 if !next_line_is_attribute {
161 let blank_lines_after = self.count_blank_lines_after(&lines, table_block.end_line);
162 if blank_lines_after < self.config.minimum_after {
163 let needed = self.config.minimum_after - blank_lines_after;
164 let message = if self.config.minimum_after == 1 {
165 "Missing blank line after table".to_string()
166 } else {
167 format!("Missing {needed} blank lines after table")
168 };
169
170 warnings.push(LintWarning {
171 rule_name: Some(self.name().to_string()),
172 message,
173 line: table_block.end_line + 1,
174 column: lines[table_block.end_line].len() + 1,
175 end_line: table_block.end_line + 1,
176 end_column: lines[table_block.end_line].len() + 2,
177 severity: Severity::Warning,
178 fix: Some(Fix {
179 range: _line_index.line_col_to_byte_range(
181 table_block.end_line + 1,
182 lines[table_block.end_line].len() + 1,
183 ),
184 replacement: "\n".repeat(needed),
185 }),
186 });
187 }
188 }
189 }
190 }
191
192 Ok(warnings)
193 }
194
195 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
196 let content = ctx.content;
197 let _line_index = &ctx.line_index;
198
199 let mut warnings = self.check(ctx)?;
200 if warnings.is_empty() {
201 return Ok(content.to_string());
202 }
203
204 let lines: Vec<&str> = content.lines().collect();
205 let mut result = Vec::new();
206 let mut i = 0;
207
208 while i < lines.len() {
209 let warning_before = warnings
211 .iter()
212 .position(|w| w.line == i + 1 && w.message.contains("before table"));
213
214 if let Some(idx) = warning_before {
215 let warning = &warnings[idx];
216 let needed_blanks = if warning.message.contains("Missing blank line before") {
218 1
219 } else if let Some(start) = warning.message.find("Missing ") {
220 if let Some(end) = warning.message.find(" blank lines before") {
221 warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
222 } else {
223 1
224 }
225 } else {
226 1
227 };
228
229 for _ in 0..needed_blanks {
231 result.push("".to_string());
232 }
233 warnings.remove(idx);
234 }
235
236 result.push(lines[i].to_string());
237
238 let warning_after = warnings
240 .iter()
241 .position(|w| w.line == i + 1 && w.message.contains("after table"));
242
243 if let Some(idx) = warning_after {
244 let warning = &warnings[idx];
245 let needed_blanks = if warning.message.contains("Missing blank line after") {
247 1
248 } else if let Some(start) = warning.message.find("Missing ") {
249 if let Some(end) = warning.message.find(" blank lines after") {
250 warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
251 } else {
252 1
253 }
254 } else {
255 1
256 };
257
258 for _ in 0..needed_blanks {
260 result.push("".to_string());
261 }
262 warnings.remove(idx);
263 }
264
265 i += 1;
266 }
267
268 Ok(result.join("\n"))
269 }
270
271 fn as_any(&self) -> &dyn std::any::Any {
272 self
273 }
274
275 fn default_config_section(&self) -> Option<(String, toml::Value)> {
276 let default_config = MD058Config::default();
277 let json_value = serde_json::to_value(&default_config).ok()?;
278 let toml_value = crate::rule_config_serde::json_to_toml_value(&json_value)?;
279 if let toml::Value::Table(table) = toml_value {
280 if !table.is_empty() {
281 Some((MD058Config::RULE_NAME.to_string(), toml::Value::Table(table)))
282 } else {
283 None
284 }
285 } else {
286 None
287 }
288 }
289
290 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
291 where
292 Self: Sized,
293 {
294 let rule_config = crate::rule_config_serde::load_rule_config::<MD058Config>(config);
295 Box::new(MD058BlanksAroundTables::from_config_struct(rule_config))
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::lint_context::LintContext;
303 use crate::utils::table_utils::TableUtils;
304
305 #[test]
306 fn test_table_with_blanks() {
307 let rule = MD058BlanksAroundTables::default();
308 let content = "Some text before.
309
310| Header 1 | Header 2 |
311|----------|----------|
312| Cell 1 | Cell 2 |
313
314Some text after.";
315 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
316 let result = rule.check(&ctx).unwrap();
317
318 assert_eq!(result.len(), 0);
319 }
320
321 #[test]
322 fn test_table_missing_blank_before() {
323 let rule = MD058BlanksAroundTables::default();
324 let content = "Some text before.
325| Header 1 | Header 2 |
326|----------|----------|
327| Cell 1 | Cell 2 |
328
329Some text after.";
330 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
331 let result = rule.check(&ctx).unwrap();
332
333 assert_eq!(result.len(), 1);
334 assert_eq!(result[0].line, 2);
335 assert!(result[0].message.contains("Missing blank line before table"));
336 }
337
338 #[test]
339 fn test_table_missing_blank_after() {
340 let rule = MD058BlanksAroundTables::default();
341 let content = "Some text before.
342
343| Header 1 | Header 2 |
344|----------|----------|
345| Cell 1 | Cell 2 |
346Some text after.";
347 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
348 let result = rule.check(&ctx).unwrap();
349
350 assert_eq!(result.len(), 1);
351 assert_eq!(result[0].line, 5);
352 assert!(result[0].message.contains("Missing blank line after table"));
353 }
354
355 #[test]
356 fn test_table_missing_both_blanks() {
357 let rule = MD058BlanksAroundTables::default();
358 let content = "Some text before.
359| Header 1 | Header 2 |
360|----------|----------|
361| Cell 1 | Cell 2 |
362Some text after.";
363 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
364 let result = rule.check(&ctx).unwrap();
365
366 assert_eq!(result.len(), 2);
367 assert!(result[0].message.contains("Missing blank line before table"));
368 assert!(result[1].message.contains("Missing blank line after table"));
369 }
370
371 #[test]
372 fn test_table_at_start_of_document() {
373 let rule = MD058BlanksAroundTables::default();
374 let content = "| Header 1 | Header 2 |
375|----------|----------|
376| Cell 1 | Cell 2 |
377
378Some text after.";
379 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
380 let result = rule.check(&ctx).unwrap();
381
382 assert_eq!(result.len(), 0);
384 }
385
386 #[test]
387 fn test_table_at_end_of_document() {
388 let rule = MD058BlanksAroundTables::default();
389 let content = "Some text before.
390
391| Header 1 | Header 2 |
392|----------|----------|
393| Cell 1 | Cell 2 |";
394 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
395 let result = rule.check(&ctx).unwrap();
396
397 assert_eq!(result.len(), 0);
399 }
400
401 #[test]
402 fn test_multiple_tables() {
403 let rule = MD058BlanksAroundTables::default();
404 let content = "Text before first table.
405| Col 1 | Col 2 |
406|--------|-------|
407| Data 1 | Val 1 |
408Text between tables.
409| Col A | Col B |
410|--------|-------|
411| Data 2 | Val 2 |
412Text after second table.";
413 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
414 let result = rule.check(&ctx).unwrap();
415
416 assert_eq!(result.len(), 4);
417 assert!(result[0].message.contains("Missing blank line before table"));
419 assert!(result[1].message.contains("Missing blank line after table"));
420 assert!(result[2].message.contains("Missing blank line before table"));
422 assert!(result[3].message.contains("Missing blank line after table"));
423 }
424
425 #[test]
426 fn test_consecutive_tables() {
427 let rule = MD058BlanksAroundTables::default();
428 let content = "Some text.
429
430| Col 1 | Col 2 |
431|--------|-------|
432| Data 1 | Val 1 |
433
434| Col A | Col B |
435|--------|-------|
436| Data 2 | Val 2 |
437
438More text.";
439 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
440 let result = rule.check(&ctx).unwrap();
441
442 assert_eq!(result.len(), 0);
444 }
445
446 #[test]
447 fn test_consecutive_tables_no_blank() {
448 let rule = MD058BlanksAroundTables::default();
449 let content = "Some text.
451
452| Col 1 | Col 2 |
453|--------|-------|
454| Data 1 | Val 1 |
455Text between.
456| Col A | Col B |
457|--------|-------|
458| Data 2 | Val 2 |
459
460More text.";
461 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
462 let result = rule.check(&ctx).unwrap();
463
464 assert_eq!(result.len(), 2);
466 assert!(result[0].message.contains("Missing blank line after table"));
467 assert!(result[1].message.contains("Missing blank line before table"));
468 }
469
470 #[test]
471 fn test_fix_missing_blanks() {
472 let rule = MD058BlanksAroundTables::default();
473 let content = "Text before.
474| Header | Col 2 |
475|--------|-------|
476| Cell | Data |
477Text after.";
478 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
479 let fixed = rule.fix(&ctx).unwrap();
480
481 let expected = "Text before.
482
483| Header | Col 2 |
484|--------|-------|
485| Cell | Data |
486
487Text after.";
488 assert_eq!(fixed, expected);
489 }
490
491 #[test]
492 fn test_fix_multiple_tables() {
493 let rule = MD058BlanksAroundTables::default();
494 let content = "Start
495| T1 | C1 |
496|----|----|
497| D1 | V1 |
498Middle
499| T2 | C2 |
500|----|----|
501| D2 | V2 |
502End";
503 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
504 let fixed = rule.fix(&ctx).unwrap();
505
506 let expected = "Start
507
508| T1 | C1 |
509|----|----|
510| D1 | V1 |
511
512Middle
513
514| T2 | C2 |
515|----|----|
516| D2 | V2 |
517
518End";
519 assert_eq!(fixed, expected);
520 }
521
522 #[test]
523 fn test_empty_content() {
524 let rule = MD058BlanksAroundTables::default();
525 let content = "";
526 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
527 let result = rule.check(&ctx).unwrap();
528
529 assert_eq!(result.len(), 0);
530 }
531
532 #[test]
533 fn test_no_tables() {
534 let rule = MD058BlanksAroundTables::default();
535 let content = "Just regular text.
536No tables here.
537Only paragraphs.";
538 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
539 let result = rule.check(&ctx).unwrap();
540
541 assert_eq!(result.len(), 0);
542 }
543
544 #[test]
545 fn test_code_block_with_table() {
546 let rule = MD058BlanksAroundTables::default();
547 let content = "Text before.
548```
549| Not | A | Table |
550|-----|---|-------|
551| In | Code | Block |
552```
553Text after.";
554 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
555 let result = rule.check(&ctx).unwrap();
556
557 assert_eq!(result.len(), 0);
559 }
560
561 #[test]
562 fn test_table_with_complex_content() {
563 let rule = MD058BlanksAroundTables::default();
564 let content = "# Heading
565| Column 1 | Column 2 | Column 3 |
566|:---------|:--------:|---------:|
567| Left | Center | Right |
568| Data | More | Info |
569## Another Heading";
570 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
571 let result = rule.check(&ctx).unwrap();
572
573 assert_eq!(result.len(), 2);
574 assert!(result[0].message.contains("Missing blank line before table"));
575 assert!(result[1].message.contains("Missing blank line after table"));
576 }
577
578 #[test]
579 fn test_table_with_empty_cells() {
580 let rule = MD058BlanksAroundTables::default();
581 let content = "Text.
582
583| | | |
584|-----|-----|-----|
585| | X | |
586| O | | X |
587
588More text.";
589 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
590 let result = rule.check(&ctx).unwrap();
591
592 assert_eq!(result.len(), 0);
593 }
594
595 #[test]
596 fn test_table_with_unicode() {
597 let rule = MD058BlanksAroundTables::default();
598 let content = "Unicode test.
599| 名前 | 年齢 | 都市 |
600|------|------|------|
601| 田中 | 25 | 東京 |
602| 佐藤 | 30 | 大阪 |
603End.";
604 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
605 let result = rule.check(&ctx).unwrap();
606
607 assert_eq!(result.len(), 2);
608 }
609
610 #[test]
611 fn test_table_with_long_cells() {
612 let rule = MD058BlanksAroundTables::default();
613 let content = "Before.
614
615| Short | Very very very very very very very very long header |
616|-------|-----------------------------------------------------|
617| Data | This is an extremely long cell content that goes on |
618
619After.";
620 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
621 let result = rule.check(&ctx).unwrap();
622
623 assert_eq!(result.len(), 0);
624 }
625
626 #[test]
627 fn test_table_without_content_rows() {
628 let rule = MD058BlanksAroundTables::default();
629 let content = "Text.
630| Header 1 | Header 2 |
631|----------|----------|
632Next paragraph.";
633 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
634 let result = rule.check(&ctx).unwrap();
635
636 assert_eq!(result.len(), 2);
638 }
639
640 #[test]
641 fn test_indented_table() {
642 let rule = MD058BlanksAroundTables::default();
643 let content = "List item:
644
645 | Indented | Table |
646 |----------|-------|
647 | Data | Here |
648
649 More content.";
650 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
651 let result = rule.check(&ctx).unwrap();
652
653 assert_eq!(result.len(), 0);
655 }
656
657 #[test]
658 fn test_single_column_table_not_detected() {
659 let rule = MD058BlanksAroundTables::default();
660 let content = "Text before.
661| Single |
662|--------|
663| Column |
664Text after.";
665 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
666 let result = rule.check(&ctx).unwrap();
667
668 assert_eq!(result.len(), 2);
671 assert!(result[0].message.contains("before"));
672 assert!(result[1].message.contains("after"));
673 }
674
675 #[test]
676 fn test_config_minimum_before() {
677 let config = MD058Config {
678 minimum_before: 2,
679 minimum_after: 1,
680 };
681 let rule = MD058BlanksAroundTables::from_config_struct(config);
682
683 let content = "Text before.
684
685| Header | Col 2 |
686|--------|-------|
687| Cell | Data |
688
689Text after.";
690 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
691 let result = rule.check(&ctx).unwrap();
692
693 assert_eq!(result.len(), 1);
695 assert!(result[0].message.contains("Missing 1 blank lines before table"));
696 }
697
698 #[test]
699 fn test_config_minimum_after() {
700 let config = MD058Config {
701 minimum_before: 1,
702 minimum_after: 3,
703 };
704 let rule = MD058BlanksAroundTables::from_config_struct(config);
705
706 let content = "Text before.
707
708| Header | Col 2 |
709|--------|-------|
710| Cell | Data |
711
712More text.";
713 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
714 let result = rule.check(&ctx).unwrap();
715
716 assert_eq!(result.len(), 1);
718 assert!(result[0].message.contains("Missing 2 blank lines after table"));
719 }
720
721 #[test]
722 fn test_config_both_minimum() {
723 let config = MD058Config {
724 minimum_before: 2,
725 minimum_after: 2,
726 };
727 let rule = MD058BlanksAroundTables::from_config_struct(config);
728
729 let content = "Text before.
730| Header | Col 2 |
731|--------|-------|
732| Cell | Data |
733More text.";
734 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
735 let result = rule.check(&ctx).unwrap();
736
737 assert_eq!(result.len(), 2);
739 assert!(result[0].message.contains("Missing 2 blank lines before table"));
740 assert!(result[1].message.contains("Missing 2 blank lines after table"));
741 }
742
743 #[test]
744 fn test_config_zero_minimum() {
745 let config = MD058Config {
746 minimum_before: 0,
747 minimum_after: 0,
748 };
749 let rule = MD058BlanksAroundTables::from_config_struct(config);
750
751 let content = "Text before.
752| Header | Col 2 |
753|--------|-------|
754| Cell | Data |
755More text.";
756 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
757 let result = rule.check(&ctx).unwrap();
758
759 assert_eq!(result.len(), 0);
761 }
762
763 #[test]
764 fn test_fix_with_custom_config() {
765 let config = MD058Config {
766 minimum_before: 2,
767 minimum_after: 3,
768 };
769 let rule = MD058BlanksAroundTables::from_config_struct(config);
770
771 let content = "Text before.
772| Header | Col 2 |
773|--------|-------|
774| Cell | Data |
775Text after.";
776 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
777 let fixed = rule.fix(&ctx).unwrap();
778
779 let expected = "Text before.
780
781
782| Header | Col 2 |
783|--------|-------|
784| Cell | Data |
785
786
787
788Text after.";
789 assert_eq!(fixed, expected);
790 }
791
792 #[test]
793 fn test_default_config_section() {
794 let rule = MD058BlanksAroundTables::default();
795 let config_section = rule.default_config_section();
796
797 assert!(config_section.is_some());
798 let (name, value) = config_section.unwrap();
799 assert_eq!(name, "MD058");
800
801 if let toml::Value::Table(table) = value {
803 assert!(table.contains_key("minimum-before"));
804 assert!(table.contains_key("minimum-after"));
805 assert_eq!(table["minimum-before"], toml::Value::Integer(1));
806 assert_eq!(table["minimum-after"], toml::Value::Integer(1));
807 } else {
808 panic!("Expected TOML table");
809 }
810 }
811
812 #[test]
813 fn test_blank_lines_counting() {
814 let rule = MD058BlanksAroundTables::default();
815 let lines = vec!["text", "", "", "table", "more", "", "end"];
816
817 assert_eq!(rule.count_blank_lines_before(&lines, 3), 2);
819
820 assert_eq!(rule.count_blank_lines_after(&lines, 4), 1);
822
823 assert_eq!(rule.count_blank_lines_before(&lines, 0), 0);
825
826 assert_eq!(rule.count_blank_lines_after(&lines, 6), 0);
828 }
829
830 #[test]
831 fn test_issue_25_table_with_long_line() {
832 let rule = MD058BlanksAroundTables::default();
834 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 |";
835 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
836
837 let table_blocks = TableUtils::find_table_blocks(content, &ctx);
839 for (i, block) in table_blocks.iter().enumerate() {
840 eprintln!(
841 "Table {}: start={}, end={}, header={}, delimiter={}, content_lines={:?}",
842 i + 1,
843 block.start_line + 1,
844 block.end_line + 1,
845 block.header_line + 1,
846 block.delimiter_line + 1,
847 block.content_lines.iter().map(|x| x + 1).collect::<Vec<_>>()
848 );
849 }
850
851 let result = rule.check(&ctx).unwrap();
852
853 assert_eq!(table_blocks.len(), 1, "Should detect exactly one table block");
855
856 assert_eq!(result.len(), 0, "Should not flag any MD058 issues for a complete table");
858 }
859}