1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::rule_config_serde::RuleConfig;
3use crate::utils::mkdocs_attr_list::is_block_attribute_line;
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 {
62 crate::utils::regex_cache::is_blank_in_blockquote_context(line)
63 }
64
65 fn count_blank_lines_before(&self, lines: &[&str], line_index: usize) -> usize {
67 let mut count = 0;
68 let mut i = line_index;
69 while i > 0 {
70 i -= 1;
71 if self.is_blank_line(lines[i]) {
72 count += 1;
73 } else {
74 break;
75 }
76 }
77 count
78 }
79
80 fn count_blank_lines_after(&self, lines: &[&str], line_index: usize) -> usize {
82 let mut count = 0;
83 let mut i = line_index + 1;
84 while i < lines.len() {
85 if self.is_blank_line(lines[i]) {
86 count += 1;
87 i += 1;
88 } else {
89 break;
90 }
91 }
92 count
93 }
94}
95
96impl Rule for MD058BlanksAroundTables {
97 fn name(&self) -> &'static str {
98 "MD058"
99 }
100
101 fn description(&self) -> &'static str {
102 "Tables should be surrounded by blank lines"
103 }
104
105 fn category(&self) -> RuleCategory {
106 RuleCategory::Table
107 }
108
109 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
110 !ctx.likely_has_tables()
112 }
113
114 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
115 let content = ctx.content;
116 let mut warnings = Vec::new();
117
118 if content.is_empty() || !content.contains('|') {
120 return Ok(Vec::new());
121 }
122
123 let lines = ctx.raw_lines();
124
125 let table_blocks = &ctx.table_blocks;
127
128 for table_block in table_blocks {
129 if table_block.start_line > 0 {
131 let blank_lines_before = self.count_blank_lines_before(lines, table_block.start_line);
132 if blank_lines_before < self.config.minimum_before {
133 let needed = self.config.minimum_before - blank_lines_before;
134 let message = if self.config.minimum_before == 1 {
135 "Missing blank line before table".to_string()
136 } else {
137 format!("Missing {needed} blank lines before table")
138 };
139
140 let bq_prefix = ctx.blockquote_prefix_for_blank_line(table_block.start_line);
141 let replacement = format!("{bq_prefix}\n").repeat(needed);
142 warnings.push(LintWarning {
143 rule_name: Some(self.name().to_string()),
144 message,
145 line: table_block.start_line + 1,
146 column: 1,
147 end_line: table_block.start_line + 1,
148 end_column: 2,
149 severity: Severity::Warning,
150 fix: Some(Fix::new(
151 ctx.line_column_byte_range(table_block.start_line + 1, 1),
152 replacement,
153 )),
154 });
155 }
156 }
157
158 if table_block.end_line < lines.len() - 1 {
160 let next_line_is_attribute = if table_block.end_line + 1 < lines.len() {
162 is_block_attribute_line(lines[table_block.end_line + 1], ctx.flavor)
163 } else {
164 false
165 };
166
167 if !next_line_is_attribute {
169 let blank_lines_after = self.count_blank_lines_after(lines, table_block.end_line);
170 if blank_lines_after < self.config.minimum_after {
171 let needed = self.config.minimum_after - blank_lines_after;
172 let message = if self.config.minimum_after == 1 {
173 "Missing blank line after table".to_string()
174 } else {
175 format!("Missing {needed} blank lines after table")
176 };
177
178 let bq_prefix = ctx.blockquote_prefix_for_blank_line(table_block.end_line);
179 let replacement = format!("{bq_prefix}\n").repeat(needed);
180 warnings.push(LintWarning {
181 rule_name: Some(self.name().to_string()),
182 message,
183 line: table_block.end_line + 1,
184 column: lines[table_block.end_line].chars().count() + 1,
185 end_line: table_block.end_line + 1,
186 end_column: lines[table_block.end_line].chars().count() + 2,
187 severity: Severity::Warning,
188 fix: Some(Fix::new(
189 ctx.line_column_byte_range(
190 table_block.end_line + 1,
191 lines[table_block.end_line].len() + 1,
192 ),
193 replacement,
194 )),
195 });
196 }
197 }
198 }
199 }
200
201 Ok(warnings)
202 }
203
204 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
205 let content = ctx.content;
206
207 let warnings = self.check(ctx)?;
208 let mut warnings =
209 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
210 if warnings.is_empty() {
211 return Ok(content.to_string());
212 }
213
214 let lines = ctx.raw_lines();
215 let mut result = Vec::new();
216 let mut i = 0;
217
218 while i < lines.len() {
219 let warning_before = warnings
221 .iter()
222 .position(|w| w.line == i + 1 && w.message.contains("before table"));
223
224 if let Some(idx) = warning_before {
225 let warning = &warnings[idx];
226 let needed_blanks = if warning.message.contains("Missing blank line before") {
228 1
229 } else if let Some(start) = warning.message.find("Missing ") {
230 if let Some(end) = warning.message.find(" blank lines before") {
231 warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
232 } else {
233 1
234 }
235 } else {
236 1
237 };
238
239 let bq_prefix = ctx.blockquote_prefix_for_blank_line(i);
241 for _ in 0..needed_blanks {
242 result.push(bq_prefix.clone());
243 }
244 warnings.remove(idx);
245 }
246
247 result.push(lines[i].to_string());
248
249 let warning_after = warnings
251 .iter()
252 .position(|w| w.line == i + 1 && w.message.contains("after table"));
253
254 if let Some(idx) = warning_after {
255 let warning = &warnings[idx];
256 let needed_blanks = if warning.message.contains("Missing blank line after") {
258 1
259 } else if let Some(start) = warning.message.find("Missing ") {
260 if let Some(end) = warning.message.find(" blank lines after") {
261 warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
262 } else {
263 1
264 }
265 } else {
266 1
267 };
268
269 let bq_prefix = ctx.blockquote_prefix_for_blank_line(i);
271 for _ in 0..needed_blanks {
272 result.push(bq_prefix.clone());
273 }
274 warnings.remove(idx);
275 }
276
277 i += 1;
278 }
279
280 let mut fixed = result.join("\n");
281 if content.ends_with('\n') {
282 fixed.push('\n');
283 }
284
285 Ok(fixed)
286 }
287
288 fn as_any(&self) -> &dyn std::any::Any {
289 self
290 }
291
292 crate::impl_rule_config_methods!(MD058Config);
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::lint_context::LintContext;
299 use crate::utils::table_utils::TableUtils;
300
301 #[test]
302 fn test_table_with_blanks() {
303 let rule = MD058BlanksAroundTables::default();
304 let content = "Some text before.
305
306| Header 1 | Header 2 |
307|----------|----------|
308| Cell 1 | Cell 2 |
309
310Some text after.";
311 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
312 let result = rule.check(&ctx).unwrap();
313
314 assert_eq!(result.len(), 0);
315 }
316
317 #[test]
318 fn test_table_missing_blank_before() {
319 let rule = MD058BlanksAroundTables::default();
320 let content = "Some text before.
321| Header 1 | Header 2 |
322|----------|----------|
323| Cell 1 | Cell 2 |
324
325Some text after.";
326 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
327 let result = rule.check(&ctx).unwrap();
328
329 assert_eq!(result.len(), 1);
330 assert_eq!(result[0].line, 2);
331 assert!(result[0].message.contains("Missing blank line before table"));
332 }
333
334 #[test]
335 fn test_table_missing_blank_after() {
336 let rule = MD058BlanksAroundTables::default();
337 let content = "Some text before.
338
339| Header 1 | Header 2 |
340|----------|----------|
341| Cell 1 | Cell 2 |
342Some text after.";
343 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
344 let result = rule.check(&ctx).unwrap();
345
346 assert_eq!(result.len(), 1);
347 assert_eq!(result[0].line, 5);
348 assert!(result[0].message.contains("Missing blank line after table"));
349 }
350
351 #[test]
352 fn test_table_missing_both_blanks() {
353 let rule = MD058BlanksAroundTables::default();
354 let content = "Some text before.
355| Header 1 | Header 2 |
356|----------|----------|
357| Cell 1 | Cell 2 |
358Some text after.";
359 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
360 let result = rule.check(&ctx).unwrap();
361
362 assert_eq!(result.len(), 2);
363 assert!(result[0].message.contains("Missing blank line before table"));
364 assert!(result[1].message.contains("Missing blank line after table"));
365 }
366
367 #[test]
368 fn test_table_at_start_of_document() {
369 let rule = MD058BlanksAroundTables::default();
370 let content = "| Header 1 | Header 2 |
371|----------|----------|
372| Cell 1 | Cell 2 |
373
374Some text after.";
375 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
376 let result = rule.check(&ctx).unwrap();
377
378 assert_eq!(result.len(), 0);
380 }
381
382 #[test]
383 fn test_table_at_end_of_document() {
384 let rule = MD058BlanksAroundTables::default();
385 let content = "Some text before.
386
387| Header 1 | Header 2 |
388|----------|----------|
389| Cell 1 | Cell 2 |";
390 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
391 let result = rule.check(&ctx).unwrap();
392
393 assert_eq!(result.len(), 0);
395 }
396
397 #[test]
398 fn test_multiple_tables() {
399 let rule = MD058BlanksAroundTables::default();
400 let content = "Text before first table.
401| Col 1 | Col 2 |
402|--------|-------|
403| Data 1 | Val 1 |
404Text between tables.
405| Col A | Col B |
406|--------|-------|
407| Data 2 | Val 2 |
408Text after second table.";
409 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
410 let result = rule.check(&ctx).unwrap();
411
412 assert_eq!(result.len(), 4);
413 assert!(result[0].message.contains("Missing blank line before table"));
415 assert!(result[1].message.contains("Missing blank line after table"));
416 assert!(result[2].message.contains("Missing blank line before table"));
418 assert!(result[3].message.contains("Missing blank line after table"));
419 }
420
421 #[test]
422 fn test_consecutive_tables() {
423 let rule = MD058BlanksAroundTables::default();
424 let content = "Some text.
425
426| Col 1 | Col 2 |
427|--------|-------|
428| Data 1 | Val 1 |
429
430| Col A | Col B |
431|--------|-------|
432| Data 2 | Val 2 |
433
434More text.";
435 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
436 let result = rule.check(&ctx).unwrap();
437
438 assert_eq!(result.len(), 0);
440 }
441
442 #[test]
443 fn test_consecutive_tables_no_blank() {
444 let rule = MD058BlanksAroundTables::default();
445 let content = "Some text.
447
448| Col 1 | Col 2 |
449|--------|-------|
450| Data 1 | Val 1 |
451Text between.
452| Col A | Col B |
453|--------|-------|
454| Data 2 | Val 2 |
455
456More text.";
457 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
458 let result = rule.check(&ctx).unwrap();
459
460 assert_eq!(result.len(), 2);
462 assert!(result[0].message.contains("Missing blank line after table"));
463 assert!(result[1].message.contains("Missing blank line before table"));
464 }
465
466 #[test]
467 fn test_fix_missing_blanks() {
468 let rule = MD058BlanksAroundTables::default();
469 let content = "Text before.
470| Header | Col 2 |
471|--------|-------|
472| Cell | Data |
473Text after.";
474 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
475 let fixed = rule.fix(&ctx).unwrap();
476
477 let expected = "Text before.
478
479| Header | Col 2 |
480|--------|-------|
481| Cell | Data |
482
483Text after.";
484 assert_eq!(fixed, expected);
485 }
486
487 #[test]
488 fn test_fix_multiple_tables() {
489 let rule = MD058BlanksAroundTables::default();
490 let content = "Start
491| T1 | C1 |
492|----|----|
493| D1 | V1 |
494Middle
495| T2 | C2 |
496|----|----|
497| D2 | V2 |
498End";
499 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
500 let fixed = rule.fix(&ctx).unwrap();
501
502 let expected = "Start
503
504| T1 | C1 |
505|----|----|
506| D1 | V1 |
507
508Middle
509
510| T2 | C2 |
511|----|----|
512| D2 | V2 |
513
514End";
515 assert_eq!(fixed, expected);
516 }
517
518 #[test]
519 fn test_empty_content() {
520 let rule = MD058BlanksAroundTables::default();
521 let content = "";
522 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
523 let result = rule.check(&ctx).unwrap();
524
525 assert_eq!(result.len(), 0);
526 }
527
528 #[test]
529 fn test_no_tables() {
530 let rule = MD058BlanksAroundTables::default();
531 let content = "Just regular text.
532No tables here.
533Only paragraphs.";
534 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
535 let result = rule.check(&ctx).unwrap();
536
537 assert_eq!(result.len(), 0);
538 }
539
540 #[test]
541 fn test_code_block_with_table() {
542 let rule = MD058BlanksAroundTables::default();
543 let content = "Text before.
544```
545| Not | A | Table |
546|-----|---|-------|
547| In | Code | Block |
548```
549Text after.";
550 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
551 let result = rule.check(&ctx).unwrap();
552
553 assert_eq!(result.len(), 0);
555 }
556
557 #[test]
558 fn test_table_with_complex_content() {
559 let rule = MD058BlanksAroundTables::default();
560 let content = "# Heading
561| Column 1 | Column 2 | Column 3 |
562|:---------|:--------:|---------:|
563| Left | Center | Right |
564| Data | More | Info |
565## Another Heading";
566 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
567 let result = rule.check(&ctx).unwrap();
568
569 assert_eq!(result.len(), 2);
570 assert!(result[0].message.contains("Missing blank line before table"));
571 assert!(result[1].message.contains("Missing blank line after table"));
572 }
573
574 #[test]
575 fn test_table_with_empty_cells() {
576 let rule = MD058BlanksAroundTables::default();
577 let content = "Text.
578
579| | | |
580|-----|-----|-----|
581| | X | |
582| O | | X |
583
584More text.";
585 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
586 let result = rule.check(&ctx).unwrap();
587
588 assert_eq!(result.len(), 0);
589 }
590
591 #[test]
592 fn test_table_with_unicode() {
593 let rule = MD058BlanksAroundTables::default();
594 let content = "Unicode test.
595| 名前 | 年齢 | 都市 |
596|------|------|------|
597| 田中 | 25 | 東京 |
598| 佐藤 | 30 | 大阪 |
599End.";
600 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
601 let result = rule.check(&ctx).unwrap();
602
603 assert_eq!(result.len(), 2);
604 }
605
606 #[test]
607 fn test_table_with_long_cells() {
608 let rule = MD058BlanksAroundTables::default();
609 let content = "Before.
610
611| Short | Very very very very very very very very long header |
612|-------|-----------------------------------------------------|
613| Data | This is an extremely long cell content that goes on |
614
615After.";
616 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
617 let result = rule.check(&ctx).unwrap();
618
619 assert_eq!(result.len(), 0);
620 }
621
622 #[test]
623 fn test_table_without_content_rows() {
624 let rule = MD058BlanksAroundTables::default();
625 let content = "Text.
626| Header 1 | Header 2 |
627|----------|----------|
628Next paragraph.";
629 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
630 let result = rule.check(&ctx).unwrap();
631
632 assert_eq!(result.len(), 2);
634 }
635
636 #[test]
637 fn test_indented_table() {
638 let rule = MD058BlanksAroundTables::default();
639 let content = "List item:
640
641 | Indented | Table |
642 |----------|-------|
643 | Data | Here |
644
645 More content.";
646 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
647 let result = rule.check(&ctx).unwrap();
648
649 assert_eq!(result.len(), 0);
651 }
652
653 #[test]
654 fn test_single_column_table_not_detected() {
655 let rule = MD058BlanksAroundTables::default();
656 let content = "Text before.
657| Single |
658|--------|
659| Column |
660Text after.";
661 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
662 let result = rule.check(&ctx).unwrap();
663
664 assert_eq!(result.len(), 2);
667 assert!(result[0].message.contains("before"));
668 assert!(result[1].message.contains("after"));
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, None);
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, None);
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, None);
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, None);
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, None);
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, None);
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
856 #[test]
857 fn test_fix_preserves_blockquote_prefix_before_table() {
858 let rule = MD058BlanksAroundTables::default();
860
861 let content = "> Text before
862> | H1 | H2 |
863> |----|---|
864> | a | b |";
865 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
866 let fixed = rule.fix(&ctx).unwrap();
867
868 let expected = "> Text before
870>
871> | H1 | H2 |
872> |----|---|
873> | a | b |";
874 assert_eq!(
875 fixed, expected,
876 "Fix should insert '>' blank line before table, not plain blank line"
877 );
878 }
879
880 #[test]
881 fn test_fix_preserves_blockquote_prefix_after_table() {
882 let rule = MD058BlanksAroundTables::default();
884
885 let content = "> | H1 | H2 |
886> |----|---|
887> | a | b |
888> Text after";
889 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
890 let fixed = rule.fix(&ctx).unwrap();
891
892 let expected = "> | H1 | H2 |
894> |----|---|
895> | a | b |
896>
897> Text after";
898 assert_eq!(
899 fixed, expected,
900 "Fix should insert '>' blank line after table, not plain blank line"
901 );
902 }
903
904 #[test]
905 fn test_fix_preserves_nested_blockquote_prefix_for_table() {
906 let rule = MD058BlanksAroundTables::default();
908
909 let content = ">> Nested quote
910>> | H1 |
911>> |----|
912>> | a |
913>> More text";
914 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
915 let fixed = rule.fix(&ctx).unwrap();
916
917 let expected = ">> Nested quote
919>>
920>> | H1 |
921>> |----|
922>> | a |
923>>
924>> More text";
925 assert_eq!(fixed, expected, "Fix should preserve nested blockquote prefix '>>'");
926 }
927
928 #[test]
929 fn test_fix_preserves_triple_nested_blockquote_prefix_for_table() {
930 let rule = MD058BlanksAroundTables::default();
932
933 let content = ">>> Triple nested
934>>> | A | B |
935>>> |---|---|
936>>> | 1 | 2 |
937>>> More text";
938 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
939 let fixed = rule.fix(&ctx).unwrap();
940
941 let expected = ">>> Triple nested
942>>>
943>>> | A | B |
944>>> |---|---|
945>>> | 1 | 2 |
946>>>
947>>> More text";
948 assert_eq!(
949 fixed, expected,
950 "Fix should preserve triple-nested blockquote prefix '>>>'"
951 );
952 }
953
954 #[test]
961 fn test_is_blank_line_with_blockquote_continuation() {
962 let rule = MD058BlanksAroundTables::default();
964
965 assert!(rule.is_blank_line(""));
967 assert!(rule.is_blank_line(" "));
968 assert!(rule.is_blank_line("\t"));
969 assert!(rule.is_blank_line(" \t "));
970
971 assert!(rule.is_blank_line(">"));
973 assert!(rule.is_blank_line("> "));
974 assert!(rule.is_blank_line("> "));
975 assert!(rule.is_blank_line(">>"));
976 assert!(rule.is_blank_line(">> "));
977 assert!(rule.is_blank_line(">>>"));
978 assert!(rule.is_blank_line("> > "));
979 assert!(rule.is_blank_line("> > > "));
980 assert!(rule.is_blank_line(" > ")); assert!(!rule.is_blank_line("text"));
984 assert!(!rule.is_blank_line("> text"));
985 assert!(!rule.is_blank_line(">> text"));
986 assert!(!rule.is_blank_line("> | table |"));
987 assert!(!rule.is_blank_line("| table |"));
988 }
989
990 #[test]
991 fn test_issue_305_no_warning_blockquote_with_existing_blank_before_table() {
992 let rule = MD058BlanksAroundTables::default();
995
996 let content = "> Text before
997>
998> | H1 | H2 |
999> |----|---|
1000> | a | b |";
1001 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1002 let result = rule.check(&ctx).unwrap();
1003
1004 assert_eq!(
1005 result.len(),
1006 0,
1007 "Should not warn when blockquote already has blank line before table"
1008 );
1009 }
1010
1011 #[test]
1012 fn test_issue_305_no_warning_blockquote_with_existing_blank_after_table() {
1013 let rule = MD058BlanksAroundTables::default();
1016
1017 let content = "> | H1 | H2 |
1018> |----|---|
1019> | a | b |
1020>
1021> Text after";
1022 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1023 let result = rule.check(&ctx).unwrap();
1024
1025 assert_eq!(
1026 result.len(),
1027 0,
1028 "Should not warn when blockquote already has blank line after table"
1029 );
1030 }
1031
1032 #[test]
1033 fn test_issue_305_no_warning_blockquote_with_both_blank_lines() {
1034 let rule = MD058BlanksAroundTables::default();
1036
1037 let content = "> The following options are available:
1038>
1039> | Option | Default | Description |
1040> |--------|-----------|-------------------|
1041> | port | 3000 | Server port |
1042> | host | localhost | Server host |";
1043 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1044 let result = rule.check(&ctx).unwrap();
1045
1046 assert_eq!(
1047 result.len(),
1048 0,
1049 "Issue #305: Should not warn for valid table inside blockquote with blank line"
1050 );
1051 }
1052
1053 #[test]
1054 fn test_issue_305_no_warning_nested_blockquote_with_blank_lines() {
1055 let rule = MD058BlanksAroundTables::default();
1057
1058 let content = ">> Nested text
1059>>
1060>> | Col1 | Col2 |
1061>> |------|------|
1062>> | val1 | val2 |
1063>>
1064>> More text";
1065 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1066 let result = rule.check(&ctx).unwrap();
1067
1068 assert_eq!(
1069 result.len(),
1070 0,
1071 "Should not warn for nested blockquote table with blank lines"
1072 );
1073 }
1074
1075 #[test]
1076 fn test_issue_305_no_warning_triple_nested_blockquote_with_blank_lines() {
1077 let rule = MD058BlanksAroundTables::default();
1079
1080 let content = ">>> Deep nesting
1081>>>
1082>>> | A | B |
1083>>> |---|---|
1084>>> | 1 | 2 |
1085>>>
1086>>> End";
1087 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1088 let result = rule.check(&ctx).unwrap();
1089
1090 assert_eq!(
1091 result.len(),
1092 0,
1093 "Should not warn for triple-nested blockquote table with blank lines"
1094 );
1095 }
1096
1097 #[test]
1098 fn test_issue_305_fix_does_not_corrupt_valid_blockquote_table() {
1099 let rule = MD058BlanksAroundTables::default();
1101
1102 let content = "> Text before
1103>
1104> | H1 | H2 |
1105> |----|---|
1106> | a | b |
1107>
1108> Text after";
1109 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1110 let fixed = rule.fix(&ctx).unwrap();
1111
1112 assert_eq!(fixed, content, "Fix should not modify already-valid blockquote table");
1113 }
1114
1115 #[test]
1116 fn test_issue_305_blockquote_blank_with_trailing_space() {
1117 let rule = MD058BlanksAroundTables::default();
1119
1120 let content = "> Text before
1122>
1123> | H1 | H2 |
1124> |----|---|
1125> | a | b |";
1126 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1127 let result = rule.check(&ctx).unwrap();
1128
1129 assert_eq!(
1130 result.len(),
1131 0,
1132 "Should recognize '> ' (with trailing space) as blank line"
1133 );
1134 }
1135
1136 #[test]
1137 fn test_issue_305_spaced_nested_blockquote() {
1138 let rule = MD058BlanksAroundTables::default();
1140
1141 let content = "> > Nested text
1142> >
1143> > | H1 |
1144> > |----|
1145> > | a |";
1146 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1147 let result = rule.check(&ctx).unwrap();
1148
1149 assert_eq!(
1150 result.len(),
1151 0,
1152 "Should recognize '> > ' style nested blockquote blank line"
1153 );
1154 }
1155
1156 #[test]
1157 fn test_mixed_regular_and_blockquote_tables() {
1158 let rule = MD058BlanksAroundTables::default();
1160
1161 let content = "# Mixed Content
1162
1163Regular table:
1164
1165| A | B |
1166|---|---|
1167| 1 | 2 |
1168
1169And a blockquote table:
1170
1171> Quote text
1172>
1173> | X | Y |
1174> |---|---|
1175> | 3 | 4 |
1176>
1177> End quote
1178
1179Final paragraph.";
1180 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1181 let result = rule.check(&ctx).unwrap();
1182
1183 assert_eq!(
1184 result.len(),
1185 0,
1186 "Should handle mixed regular and blockquote tables correctly"
1187 );
1188 }
1189
1190 #[test]
1191 fn test_blockquote_table_at_document_start() {
1192 let rule = MD058BlanksAroundTables::default();
1194
1195 let content = "> | H1 | H2 |
1196> |----|---|
1197> | a | b |
1198>
1199> Text after";
1200 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1201 let result = rule.check(&ctx).unwrap();
1202
1203 assert_eq!(
1204 result.len(),
1205 0,
1206 "Should not require blank line before table at document start (even in blockquote)"
1207 );
1208 }
1209
1210 #[test]
1211 fn test_blockquote_table_at_document_end() {
1212 let rule = MD058BlanksAroundTables::default();
1214
1215 let content = "> Text before
1216>
1217> | H1 | H2 |
1218> |----|---|
1219> | a | b |";
1220 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1221 let result = rule.check(&ctx).unwrap();
1222
1223 assert_eq!(
1224 result.len(),
1225 0,
1226 "Should not require blank line after table at document end"
1227 );
1228 }
1229
1230 #[test]
1231 fn test_blockquote_table_missing_blank_still_detected() {
1232 let rule = MD058BlanksAroundTables::default();
1234
1235 let content = "> Text before
1236> | H1 | H2 |
1237> |----|---|
1238> | a | b |
1239> Text after";
1240 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1241 let result = rule.check(&ctx).unwrap();
1242
1243 assert_eq!(
1245 result.len(),
1246 2,
1247 "Should still detect missing blank lines in blockquote tables"
1248 );
1249 assert!(result[0].message.contains("before table"));
1250 assert!(result[1].message.contains("after table"));
1251 }
1252
1253 #[test]
1254 fn test_blockquote_table_fix_adds_correct_prefix() {
1255 let rule = MD058BlanksAroundTables::default();
1257
1258 let content = "> Text before
1259> | H1 | H2 |
1260> |----|---|
1261> | a | b |
1262> Text after";
1263 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1264 let fixed = rule.fix(&ctx).unwrap();
1265
1266 let expected = "> Text before
1267>
1268> | H1 | H2 |
1269> |----|---|
1270> | a | b |
1271>
1272> Text after";
1273 assert_eq!(fixed, expected, "Fix should add blockquote-prefixed blank lines");
1274 }
1275
1276 #[test]
1277 fn test_multiple_blockquote_tables_with_valid_spacing() {
1278 let rule = MD058BlanksAroundTables::default();
1280
1281 let content = "> First table:
1282>
1283> | A | B |
1284> |---|---|
1285> | 1 | 2 |
1286>
1287> Second table:
1288>
1289> | X | Y |
1290> |---|---|
1291> | 3 | 4 |
1292>
1293> End";
1294 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1295 let result = rule.check(&ctx).unwrap();
1296
1297 assert_eq!(
1298 result.len(),
1299 0,
1300 "Should handle multiple blockquote tables with valid spacing"
1301 );
1302 }
1303
1304 #[test]
1305 fn test_blockquote_table_with_minimum_before_config() {
1306 let config = MD058Config {
1308 minimum_before: 2,
1309 minimum_after: 1,
1310 };
1311 let rule = MD058BlanksAroundTables::from_config_struct(config);
1312
1313 let content = "> Text
1314>
1315> | H1 |
1316> |----|
1317> | a |";
1318 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1319 let result = rule.check(&ctx).unwrap();
1320
1321 assert_eq!(result.len(), 1);
1323 assert!(result[0].message.contains("before table"));
1324 }
1325
1326 #[test]
1342 fn md058_pandoc_grid_tables_not_flagged() {
1343 let rule = MD058BlanksAroundTables::default();
1344 let content = "Some text before.
1347+---+---+
1348| a | b |
1349+===+===+
1350| 1 | 2 |
1351+---+---+
1352Some text after.";
1353
1354 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1356 let result = rule.check(&ctx).unwrap();
1357 assert!(
1358 result.is_empty(),
1359 "MD058 should not flag blank lines around Pandoc grid tables (excluded by table_blocks): {result:?}"
1360 );
1361
1362 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1364 let result_std = rule.check(&ctx_std).unwrap();
1365 assert!(
1366 result_std.is_empty(),
1367 "MD058 should not flag grid-table-like content under Standard: {result_std:?}"
1368 );
1369 }
1370
1371 #[test]
1372 fn md058_pandoc_multi_line_tables_not_flagged() {
1373 let rule = MD058BlanksAroundTables::default();
1374 let content = "Some text.
1375--------- -----------
1376Header 1 Header 2
1377--------- -----------
1378Cell 1 Cell 2
1379--------- -----------
1380More text.";
1381
1382 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1383 let result = rule.check(&ctx).unwrap();
1384 assert!(
1385 result.is_empty(),
1386 "MD058 should not flag Pandoc multi-line tables: {result:?}"
1387 );
1388
1389 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1390 let result_std = rule.check(&ctx_std).unwrap();
1391 assert!(
1392 result_std.is_empty(),
1393 "MD058 should not flag multi-line table content under Standard: {result_std:?}"
1394 );
1395 }
1396
1397 #[test]
1398 fn md058_pandoc_line_blocks_not_flagged() {
1399 let rule = MD058BlanksAroundTables::default();
1400 let content = "Some text.
1402| First line
1403| Second line
1404More text.";
1405
1406 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1407 let result = rule.check(&ctx).unwrap();
1408 assert!(
1409 result.is_empty(),
1410 "MD058 should not treat Pandoc line blocks as tables: {result:?}"
1411 );
1412
1413 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1414 let result_std = rule.check(&ctx_std).unwrap();
1415 assert!(
1416 result_std.is_empty(),
1417 "MD058 should not treat line-block-like content as tables under Standard: {result_std:?}"
1418 );
1419 }
1420
1421 #[test]
1422 fn md058_pandoc_pipe_table_captions_not_flagged() {
1423 let rule = MD058BlanksAroundTables::default();
1424 let content = "\
1427Some text.
1428
1429| H1 | H2 |
1430|----|-----|
1431| a | b |
1432
1433: My table caption
1434More text.";
1435
1436 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1437 let result = rule.check(&ctx).unwrap();
1438 assert!(
1439 result.is_empty(),
1440 "MD058 should not flag the pipe-table caption line as needing blank lines: {result:?}"
1441 );
1442
1443 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1446 let result_std = rule.check(&ctx_std).unwrap();
1447 assert!(
1448 result_std.is_empty(),
1449 "MD058 table with caption — caption not a table row under Standard: {result_std:?}"
1450 );
1451 }
1452
1453 #[test]
1454 fn md058_hugo_block_attribute_after_table_not_flagged() {
1455 let rule = MD058BlanksAroundTables::default();
1458 let content = "\
1459Some text.
1460
1461| H1 | H2 |
1462|----|-----|
1463| a | b |
1464{class=\"table table-striped\"}
1465
1466More text.";
1467
1468 for flavor in [
1469 crate::config::MarkdownFlavor::Hugo,
1470 crate::config::MarkdownFlavor::MkDocs,
1471 crate::config::MarkdownFlavor::Kramdown,
1472 ] {
1473 let ctx = LintContext::new(content, flavor, None);
1474 let result = rule.check(&ctx).unwrap();
1475 assert!(
1476 result.is_empty(),
1477 "MD058 should not flag the block attribute line under {flavor:?}: {result:?}"
1478 );
1479 }
1480
1481 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1484 let result_std = rule.check(&ctx_std).unwrap();
1485 assert_eq!(
1486 result_std.len(),
1487 1,
1488 "MD058 must flag the missing blank after table under Standard: {result_std:?}"
1489 );
1490 assert!(result_std[0].message.contains("Missing blank line after table"));
1491 }
1492
1493 #[test]
1494 fn test_fix_preserves_trailing_newline() {
1495 let rule = MD058BlanksAroundTables::default();
1496
1497 let content = "Intro\n| a | b |\n| --- | --- |\n| 1 | 2 |\nAfter\n";
1498 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1499 let fixed = rule.fix(&ctx).unwrap();
1500
1501 assert!(fixed.ends_with('\n'), "Fix should preserve trailing newline");
1502 assert_eq!(fixed, "Intro\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n\nAfter\n");
1503 }
1504
1505 #[test]
1506 fn test_fix_preserves_no_trailing_newline() {
1507 let rule = MD058BlanksAroundTables::default();
1508
1509 let content = "Intro\n| a | b |\n| --- | --- |\n| 1 | 2 |\nAfter";
1510 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1511 let fixed = rule.fix(&ctx).unwrap();
1512
1513 assert!(
1514 !fixed.ends_with('\n'),
1515 "Fix should not add trailing newline if original didn't have one"
1516 );
1517 assert_eq!(fixed, "Intro\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n\nAfter");
1518 }
1519}