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 crate::utils::range_utils::LineIndex;
5use crate::utils::table_utils::TableUtils;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16#[serde(rename_all = "kebab-case")]
17pub struct MD058Config {
18 #[serde(default = "default_minimum_before")]
20 pub minimum_before: usize,
21 #[serde(default = "default_minimum_after")]
23 pub minimum_after: usize,
24}
25
26impl Default for MD058Config {
27 fn default() -> Self {
28 Self {
29 minimum_before: default_minimum_before(),
30 minimum_after: default_minimum_after(),
31 }
32 }
33}
34
35fn default_minimum_before() -> usize {
36 1
37}
38
39fn default_minimum_after() -> usize {
40 1
41}
42
43impl RuleConfig for MD058Config {
44 const RULE_NAME: &'static str = "MD058";
45}
46
47#[derive(Clone, Default)]
48pub struct MD058BlanksAroundTables {
49 config: MD058Config,
50}
51
52impl MD058BlanksAroundTables {
53 pub fn from_config_struct(config: MD058Config) -> Self {
55 Self { config }
56 }
57
58 fn is_blank_line(&self, line: &str) -> bool {
60 line.trim().is_empty()
61 }
62
63 fn count_blank_lines_before(&self, lines: &[&str], line_index: usize) -> usize {
65 let mut count = 0;
66 let mut i = line_index;
67 while i > 0 {
68 i -= 1;
69 if self.is_blank_line(lines[i]) {
70 count += 1;
71 } else {
72 break;
73 }
74 }
75 count
76 }
77
78 fn count_blank_lines_after(&self, lines: &[&str], line_index: usize) -> usize {
80 let mut count = 0;
81 let mut i = line_index + 1;
82 while i < lines.len() {
83 if self.is_blank_line(lines[i]) {
84 count += 1;
85 i += 1;
86 } else {
87 break;
88 }
89 }
90 count
91 }
92}
93
94impl Rule for MD058BlanksAroundTables {
95 fn name(&self) -> &'static str {
96 "MD058"
97 }
98
99 fn description(&self) -> &'static str {
100 "Tables should be surrounded by blank lines"
101 }
102
103 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
104 !ctx.likely_has_tables()
106 }
107
108 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
109 let content = ctx.content;
110 let _line_index = LineIndex::new(content.to_string());
111 let mut warnings = Vec::new();
112
113 if content.is_empty() || !content.contains('|') {
115 return Ok(Vec::new());
116 }
117
118 let lines: Vec<&str> = content.lines().collect();
119
120 let table_blocks = TableUtils::find_table_blocks(content, ctx);
122
123 for table_block in table_blocks {
124 if table_block.start_line > 0 {
126 let blank_lines_before = self.count_blank_lines_before(&lines, table_block.start_line);
127 if blank_lines_before < self.config.minimum_before {
128 let needed = self.config.minimum_before - blank_lines_before;
129 let message = if self.config.minimum_before == 1 {
130 "Missing blank line before table".to_string()
131 } else {
132 format!("Missing {needed} blank lines before table")
133 };
134
135 warnings.push(LintWarning {
136 rule_name: Some(self.name()),
137 message,
138 line: table_block.start_line + 1,
139 column: 1,
140 end_line: table_block.start_line + 1,
141 end_column: 2,
142 severity: Severity::Warning,
143 fix: Some(Fix {
144 range: _line_index.line_col_to_byte_range(table_block.start_line + 1, 1),
145 replacement: format!("{}{}", "\n".repeat(needed), lines[table_block.start_line]),
146 }),
147 });
148 }
149 }
150
151 if table_block.end_line < lines.len() - 1 {
153 let next_line_is_attribute = if table_block.end_line + 1 < lines.len() {
155 is_kramdown_block_attribute(lines[table_block.end_line + 1])
156 } else {
157 false
158 };
159
160 if !next_line_is_attribute {
162 let blank_lines_after = self.count_blank_lines_after(&lines, table_block.end_line);
163 if blank_lines_after < self.config.minimum_after {
164 let needed = self.config.minimum_after - blank_lines_after;
165 let message = if self.config.minimum_after == 1 {
166 "Missing blank line after table".to_string()
167 } else {
168 format!("Missing {needed} blank lines after table")
169 };
170
171 warnings.push(LintWarning {
172 rule_name: Some(self.name()),
173 message,
174 line: table_block.end_line + 1,
175 column: lines[table_block.end_line].len() + 1,
176 end_line: table_block.end_line + 1,
177 end_column: lines[table_block.end_line].len() + 2,
178 severity: Severity::Warning,
179 fix: Some(Fix {
180 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: format!("{}{}", lines[table_block.end_line], "\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 = LineIndex::new(content.to_string());
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
304 #[test]
305 fn test_table_with_blanks() {
306 let rule = MD058BlanksAroundTables::default();
307 let content = "Some text before.
308
309| Header 1 | Header 2 |
310|----------|----------|
311| Cell 1 | Cell 2 |
312
313Some text after.";
314 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
315 let result = rule.check(&ctx).unwrap();
316
317 assert_eq!(result.len(), 0);
318 }
319
320 #[test]
321 fn test_table_missing_blank_before() {
322 let rule = MD058BlanksAroundTables::default();
323 let content = "Some text before.
324| Header 1 | Header 2 |
325|----------|----------|
326| Cell 1 | Cell 2 |
327
328Some text after.";
329 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
330 let result = rule.check(&ctx).unwrap();
331
332 assert_eq!(result.len(), 1);
333 assert_eq!(result[0].line, 2);
334 assert!(result[0].message.contains("Missing blank line before table"));
335 }
336
337 #[test]
338 fn test_table_missing_blank_after() {
339 let rule = MD058BlanksAroundTables::default();
340 let content = "Some text before.
341
342| Header 1 | Header 2 |
343|----------|----------|
344| Cell 1 | Cell 2 |
345Some text after.";
346 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
347 let result = rule.check(&ctx).unwrap();
348
349 assert_eq!(result.len(), 1);
350 assert_eq!(result[0].line, 5);
351 assert!(result[0].message.contains("Missing blank line after table"));
352 }
353
354 #[test]
355 fn test_table_missing_both_blanks() {
356 let rule = MD058BlanksAroundTables::default();
357 let content = "Some text before.
358| Header 1 | Header 2 |
359|----------|----------|
360| Cell 1 | Cell 2 |
361Some text after.";
362 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
363 let result = rule.check(&ctx).unwrap();
364
365 assert_eq!(result.len(), 2);
366 assert!(result[0].message.contains("Missing blank line before table"));
367 assert!(result[1].message.contains("Missing blank line after table"));
368 }
369
370 #[test]
371 fn test_table_at_start_of_document() {
372 let rule = MD058BlanksAroundTables::default();
373 let content = "| Header 1 | Header 2 |
374|----------|----------|
375| Cell 1 | Cell 2 |
376
377Some text after.";
378 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
379 let result = rule.check(&ctx).unwrap();
380
381 assert_eq!(result.len(), 0);
383 }
384
385 #[test]
386 fn test_table_at_end_of_document() {
387 let rule = MD058BlanksAroundTables::default();
388 let content = "Some text before.
389
390| Header 1 | Header 2 |
391|----------|----------|
392| Cell 1 | Cell 2 |";
393 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
394 let result = rule.check(&ctx).unwrap();
395
396 assert_eq!(result.len(), 0);
398 }
399
400 #[test]
401 fn test_multiple_tables() {
402 let rule = MD058BlanksAroundTables::default();
403 let content = "Text before first table.
404| Col 1 | Col 2 |
405|--------|-------|
406| Data 1 | Val 1 |
407Text between tables.
408| Col A | Col B |
409|--------|-------|
410| Data 2 | Val 2 |
411Text after second table.";
412 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
413 let result = rule.check(&ctx).unwrap();
414
415 assert_eq!(result.len(), 4);
416 assert!(result[0].message.contains("Missing blank line before table"));
418 assert!(result[1].message.contains("Missing blank line after table"));
419 assert!(result[2].message.contains("Missing blank line before table"));
421 assert!(result[3].message.contains("Missing blank line after table"));
422 }
423
424 #[test]
425 fn test_consecutive_tables() {
426 let rule = MD058BlanksAroundTables::default();
427 let content = "Some text.
428
429| Col 1 | Col 2 |
430|--------|-------|
431| Data 1 | Val 1 |
432
433| Col A | Col B |
434|--------|-------|
435| Data 2 | Val 2 |
436
437More text.";
438 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
439 let result = rule.check(&ctx).unwrap();
440
441 assert_eq!(result.len(), 0);
443 }
444
445 #[test]
446 fn test_consecutive_tables_no_blank() {
447 let rule = MD058BlanksAroundTables::default();
448 let content = "Some text.
450
451| Col 1 | Col 2 |
452|--------|-------|
453| Data 1 | Val 1 |
454Text between.
455| Col A | Col B |
456|--------|-------|
457| Data 2 | Val 2 |
458
459More text.";
460 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
461 let result = rule.check(&ctx).unwrap();
462
463 assert_eq!(result.len(), 2);
465 assert!(result[0].message.contains("Missing blank line after table"));
466 assert!(result[1].message.contains("Missing blank line before table"));
467 }
468
469 #[test]
470 fn test_fix_missing_blanks() {
471 let rule = MD058BlanksAroundTables::default();
472 let content = "Text before.
473| Header | Col 2 |
474|--------|-------|
475| Cell | Data |
476Text after.";
477 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
478 let fixed = rule.fix(&ctx).unwrap();
479
480 let expected = "Text before.
481
482| Header | Col 2 |
483|--------|-------|
484| Cell | Data |
485
486Text after.";
487 assert_eq!(fixed, expected);
488 }
489
490 #[test]
491 fn test_fix_multiple_tables() {
492 let rule = MD058BlanksAroundTables::default();
493 let content = "Start
494| T1 | C1 |
495|----|----|
496| D1 | V1 |
497Middle
498| T2 | C2 |
499|----|----|
500| D2 | V2 |
501End";
502 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
503 let fixed = rule.fix(&ctx).unwrap();
504
505 let expected = "Start
506
507| T1 | C1 |
508|----|----|
509| D1 | V1 |
510
511Middle
512
513| T2 | C2 |
514|----|----|
515| D2 | V2 |
516
517End";
518 assert_eq!(fixed, expected);
519 }
520
521 #[test]
522 fn test_empty_content() {
523 let rule = MD058BlanksAroundTables::default();
524 let content = "";
525 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
526 let result = rule.check(&ctx).unwrap();
527
528 assert_eq!(result.len(), 0);
529 }
530
531 #[test]
532 fn test_no_tables() {
533 let rule = MD058BlanksAroundTables::default();
534 let content = "Just regular text.
535No tables here.
536Only paragraphs.";
537 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
538 let result = rule.check(&ctx).unwrap();
539
540 assert_eq!(result.len(), 0);
541 }
542
543 #[test]
544 fn test_code_block_with_table() {
545 let rule = MD058BlanksAroundTables::default();
546 let content = "Text before.
547```
548| Not | A | Table |
549|-----|---|-------|
550| In | Code | Block |
551```
552Text after.";
553 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
554 let result = rule.check(&ctx).unwrap();
555
556 assert_eq!(result.len(), 0);
558 }
559
560 #[test]
561 fn test_table_with_complex_content() {
562 let rule = MD058BlanksAroundTables::default();
563 let content = "# Heading
564| Column 1 | Column 2 | Column 3 |
565|:---------|:--------:|---------:|
566| Left | Center | Right |
567| Data | More | Info |
568## Another Heading";
569 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
570 let result = rule.check(&ctx).unwrap();
571
572 assert_eq!(result.len(), 2);
573 assert!(result[0].message.contains("Missing blank line before table"));
574 assert!(result[1].message.contains("Missing blank line after table"));
575 }
576
577 #[test]
578 fn test_table_with_empty_cells() {
579 let rule = MD058BlanksAroundTables::default();
580 let content = "Text.
581
582| | | |
583|-----|-----|-----|
584| | X | |
585| O | | X |
586
587More text.";
588 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
589 let result = rule.check(&ctx).unwrap();
590
591 assert_eq!(result.len(), 0);
592 }
593
594 #[test]
595 fn test_table_with_unicode() {
596 let rule = MD058BlanksAroundTables::default();
597 let content = "Unicode test.
598| 名前 | 年齢 | 都市 |
599|------|------|------|
600| 田中 | 25 | 東京 |
601| 佐藤 | 30 | 大阪 |
602End.";
603 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
604 let result = rule.check(&ctx).unwrap();
605
606 assert_eq!(result.len(), 2);
607 }
608
609 #[test]
610 fn test_table_with_long_cells() {
611 let rule = MD058BlanksAroundTables::default();
612 let content = "Before.
613
614| Short | Very very very very very very very very long header |
615|-------|-----------------------------------------------------|
616| Data | This is an extremely long cell content that goes on |
617
618After.";
619 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
620 let result = rule.check(&ctx).unwrap();
621
622 assert_eq!(result.len(), 0);
623 }
624
625 #[test]
626 fn test_table_without_content_rows() {
627 let rule = MD058BlanksAroundTables::default();
628 let content = "Text.
629| Header 1 | Header 2 |
630|----------|----------|
631Next paragraph.";
632 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
633 let result = rule.check(&ctx).unwrap();
634
635 assert_eq!(result.len(), 2);
637 }
638
639 #[test]
640 fn test_indented_table() {
641 let rule = MD058BlanksAroundTables::default();
642 let content = "List item:
643
644 | Indented | Table |
645 |----------|-------|
646 | Data | Here |
647
648 More content.";
649 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
650 let result = rule.check(&ctx).unwrap();
651
652 assert_eq!(result.len(), 0);
654 }
655
656 #[test]
657 fn test_single_column_table_not_detected() {
658 let rule = MD058BlanksAroundTables::default();
659 let content = "Text before.
660| Single |
661|--------|
662| Column |
663Text after.";
664 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
665 let result = rule.check(&ctx).unwrap();
666
667 assert_eq!(result.len(), 0);
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);
687 let result = rule.check(&ctx).unwrap();
688
689 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);
710 let result = rule.check(&ctx).unwrap();
711
712 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);
731 let result = rule.check(&ctx).unwrap();
732
733 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);
753 let result = rule.check(&ctx).unwrap();
754
755 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);
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 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 assert_eq!(rule.count_blank_lines_before(&lines, 3), 2);
815
816 assert_eq!(rule.count_blank_lines_after(&lines, 4), 1);
818
819 assert_eq!(rule.count_blank_lines_before(&lines, 0), 0);
821
822 assert_eq!(rule.count_blank_lines_after(&lines, 6), 0);
824 }
825
826 #[test]
827 fn test_issue_25_table_with_long_line() {
828 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);
832
833 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 assert_eq!(table_blocks.len(), 1, "Should detect exactly one table block");
851
852 assert_eq!(result.len(), 0, "Should not flag any MD058 issues for a complete table");
854 }
855}