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