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