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 {
65 crate::utils::regex_cache::is_blank_in_blockquote_context(line)
66 || crate::utils::blank_lines::is_blank_or_comment_only(line)
67 }
68
69 fn count_blank_lines_before(&self, lines: &[&str], line_index: usize) -> usize {
71 let mut count = 0;
72 let mut i = line_index;
73 while i > 0 {
74 i -= 1;
75 if self.is_blank_line(lines[i]) {
76 count += 1;
77 } else {
78 break;
79 }
80 }
81 count
82 }
83
84 fn count_blank_lines_after(&self, lines: &[&str], line_index: usize) -> usize {
86 let mut count = 0;
87 let mut i = line_index + 1;
88 while i < lines.len() {
89 if self.is_blank_line(lines[i]) {
90 count += 1;
91 i += 1;
92 } else {
93 break;
94 }
95 }
96 count
97 }
98}
99
100impl Rule for MD058BlanksAroundTables {
101 fn name(&self) -> &'static str {
102 "MD058"
103 }
104
105 fn description(&self) -> &'static str {
106 "Tables should be surrounded by blank lines"
107 }
108
109 fn category(&self) -> RuleCategory {
110 RuleCategory::Table
111 }
112
113 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
114 !ctx.likely_has_tables()
116 }
117
118 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
119 let content = ctx.content;
120 let mut warnings = Vec::new();
121
122 if content.is_empty() || !content.contains('|') {
124 return Ok(Vec::new());
125 }
126
127 let lines = ctx.raw_lines();
128
129 let table_blocks = &ctx.table_blocks;
131
132 for table_block in table_blocks {
133 if table_block.start_line > 0 {
135 let blank_lines_before = self.count_blank_lines_before(lines, table_block.start_line);
136 if blank_lines_before < self.config.minimum_before {
137 let needed = self.config.minimum_before - blank_lines_before;
138 let message = if self.config.minimum_before == 1 {
139 "Missing blank line before table".to_string()
140 } else {
141 format!("Missing {needed} blank lines before table")
142 };
143
144 let bq_prefix = ctx.blockquote_prefix_for_blank_line(table_block.start_line);
145 let replacement = format!("{bq_prefix}\n").repeat(needed);
146 warnings.push(LintWarning {
147 rule_name: Some(self.name().to_string()),
148 message,
149 line: table_block.start_line + 1,
150 column: 1,
151 end_line: table_block.start_line + 1,
152 end_column: 2,
153 severity: Severity::Warning,
154 fix: Some(Fix::new(
155 ctx.line_column_byte_range(table_block.start_line + 1, 1),
156 replacement,
157 )),
158 });
159 }
160 }
161
162 if table_block.end_line < lines.len() - 1 {
164 let next_line_is_attribute = if table_block.end_line + 1 < lines.len() {
166 is_block_attribute_line(lines[table_block.end_line + 1], ctx.flavor)
167 } else {
168 false
169 };
170
171 if !next_line_is_attribute {
173 let blank_lines_after = self.count_blank_lines_after(lines, table_block.end_line);
174 if blank_lines_after < self.config.minimum_after {
175 let needed = self.config.minimum_after - blank_lines_after;
176 let message = if self.config.minimum_after == 1 {
177 "Missing blank line after table".to_string()
178 } else {
179 format!("Missing {needed} blank lines after table")
180 };
181
182 let bq_prefix = ctx.blockquote_prefix_for_blank_line(table_block.end_line);
183 let replacement = format!("{bq_prefix}\n").repeat(needed);
184 warnings.push(LintWarning {
185 rule_name: Some(self.name().to_string()),
186 message,
187 line: table_block.end_line + 1,
188 column: lines[table_block.end_line].chars().count() + 1,
189 end_line: table_block.end_line + 1,
190 end_column: lines[table_block.end_line].chars().count() + 2,
191 severity: Severity::Warning,
192 fix: Some(Fix::new(
193 ctx.line_column_byte_range(
194 table_block.end_line + 1,
195 lines[table_block.end_line].len() + 1,
196 ),
197 replacement,
198 )),
199 });
200 }
201 }
202 }
203 }
204
205 Ok(warnings)
206 }
207
208 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
209 let content = ctx.content;
210
211 let warnings = self.check(ctx)?;
212 let mut warnings =
213 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
214 if warnings.is_empty() {
215 return Ok(content.to_string());
216 }
217
218 let lines = ctx.raw_lines();
219 let mut result = Vec::new();
220 let mut i = 0;
221
222 while i < lines.len() {
223 let warning_before = warnings
225 .iter()
226 .position(|w| w.line == i + 1 && w.message.contains("before table"));
227
228 if let Some(idx) = warning_before {
229 let warning = &warnings[idx];
230 let needed_blanks = if warning.message.contains("Missing blank line before") {
232 1
233 } else if let Some(start) = warning.message.find("Missing ") {
234 if let Some(end) = warning.message.find(" blank lines before") {
235 warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
236 } else {
237 1
238 }
239 } else {
240 1
241 };
242
243 let bq_prefix = ctx.blockquote_prefix_for_blank_line(i);
245 for _ in 0..needed_blanks {
246 result.push(bq_prefix.clone());
247 }
248 warnings.remove(idx);
249 }
250
251 result.push(lines[i].to_string());
252
253 let warning_after = warnings
255 .iter()
256 .position(|w| w.line == i + 1 && w.message.contains("after table"));
257
258 if let Some(idx) = warning_after {
259 let warning = &warnings[idx];
260 let needed_blanks = if warning.message.contains("Missing blank line after") {
262 1
263 } else if let Some(start) = warning.message.find("Missing ") {
264 if let Some(end) = warning.message.find(" blank lines after") {
265 warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
266 } else {
267 1
268 }
269 } else {
270 1
271 };
272
273 let bq_prefix = ctx.blockquote_prefix_for_blank_line(i);
275 for _ in 0..needed_blanks {
276 result.push(bq_prefix.clone());
277 }
278 warnings.remove(idx);
279 }
280
281 i += 1;
282 }
283
284 let mut fixed = result.join("\n");
285 if content.ends_with('\n') {
286 fixed.push('\n');
287 }
288
289 Ok(fixed)
290 }
291
292 fn as_any(&self) -> &dyn std::any::Any {
293 self
294 }
295
296 crate::impl_rule_config_methods!(MD058Config);
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::lint_context::LintContext;
303 use crate::utils::table_utils::TableUtils;
304
305 #[test]
306 fn test_table_with_blanks() {
307 let rule = MD058BlanksAroundTables::default();
308 let content = "Some text before.
309
310| Header 1 | Header 2 |
311|----------|----------|
312| Cell 1 | Cell 2 |
313
314Some text after.";
315 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
316 let result = rule.check(&ctx).unwrap();
317
318 assert_eq!(result.len(), 0);
319 }
320
321 #[test]
322 fn test_table_missing_blank_before() {
323 let rule = MD058BlanksAroundTables::default();
324 let content = "Some text before.
325| Header 1 | Header 2 |
326|----------|----------|
327| Cell 1 | Cell 2 |
328
329Some text after.";
330 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
331 let result = rule.check(&ctx).unwrap();
332
333 assert_eq!(result.len(), 1);
334 assert_eq!(result[0].line, 2);
335 assert!(result[0].message.contains("Missing blank line before table"));
336 }
337
338 #[test]
339 fn test_table_missing_blank_after() {
340 let rule = MD058BlanksAroundTables::default();
341 let content = "Some text before.
342
343| Header 1 | Header 2 |
344|----------|----------|
345| Cell 1 | Cell 2 |
346Some text after.";
347 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
348 let result = rule.check(&ctx).unwrap();
349
350 assert_eq!(result.len(), 1);
351 assert_eq!(result[0].line, 5);
352 assert!(result[0].message.contains("Missing blank line after table"));
353 }
354
355 #[test]
356 fn test_table_missing_both_blanks() {
357 let rule = MD058BlanksAroundTables::default();
358 let content = "Some text before.
359| Header 1 | Header 2 |
360|----------|----------|
361| Cell 1 | Cell 2 |
362Some text after.";
363 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
364 let result = rule.check(&ctx).unwrap();
365
366 assert_eq!(result.len(), 2);
367 assert!(result[0].message.contains("Missing blank line before table"));
368 assert!(result[1].message.contains("Missing blank line after table"));
369 }
370
371 #[test]
372 fn test_table_at_start_of_document() {
373 let rule = MD058BlanksAroundTables::default();
374 let content = "| Header 1 | Header 2 |
375|----------|----------|
376| Cell 1 | Cell 2 |
377
378Some text after.";
379 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
380 let result = rule.check(&ctx).unwrap();
381
382 assert_eq!(result.len(), 0);
384 }
385
386 #[test]
387 fn test_table_at_end_of_document() {
388 let rule = MD058BlanksAroundTables::default();
389 let content = "Some text before.
390
391| Header 1 | Header 2 |
392|----------|----------|
393| Cell 1 | Cell 2 |";
394 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
395 let result = rule.check(&ctx).unwrap();
396
397 assert_eq!(result.len(), 0);
399 }
400
401 #[test]
402 fn test_multiple_tables() {
403 let rule = MD058BlanksAroundTables::default();
404 let content = "Text before first table.
405| Col 1 | Col 2 |
406|--------|-------|
407| Data 1 | Val 1 |
408Text between tables.
409| Col A | Col B |
410|--------|-------|
411| Data 2 | Val 2 |
412Text after second table.";
413 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
414 let result = rule.check(&ctx).unwrap();
415
416 assert_eq!(result.len(), 4);
417 assert!(result[0].message.contains("Missing blank line before table"));
419 assert!(result[1].message.contains("Missing blank line after table"));
420 assert!(result[2].message.contains("Missing blank line before table"));
422 assert!(result[3].message.contains("Missing blank line after table"));
423 }
424
425 #[test]
426 fn test_consecutive_tables() {
427 let rule = MD058BlanksAroundTables::default();
428 let content = "Some text.
429
430| Col 1 | Col 2 |
431|--------|-------|
432| Data 1 | Val 1 |
433
434| Col A | Col B |
435|--------|-------|
436| Data 2 | Val 2 |
437
438More text.";
439 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
440 let result = rule.check(&ctx).unwrap();
441
442 assert_eq!(result.len(), 0);
444 }
445
446 #[test]
447 fn test_consecutive_tables_no_blank() {
448 let rule = MD058BlanksAroundTables::default();
449 let content = "Some text.
451
452| Col 1 | Col 2 |
453|--------|-------|
454| Data 1 | Val 1 |
455Text between.
456| Col A | Col B |
457|--------|-------|
458| Data 2 | Val 2 |
459
460More text.";
461 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
462 let result = rule.check(&ctx).unwrap();
463
464 assert_eq!(result.len(), 2);
466 assert!(result[0].message.contains("Missing blank line after table"));
467 assert!(result[1].message.contains("Missing blank line before table"));
468 }
469
470 #[test]
471 fn test_fix_missing_blanks() {
472 let rule = MD058BlanksAroundTables::default();
473 let content = "Text before.
474| Header | Col 2 |
475|--------|-------|
476| Cell | Data |
477Text after.";
478 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
479 let fixed = rule.fix(&ctx).unwrap();
480
481 let expected = "Text before.
482
483| Header | Col 2 |
484|--------|-------|
485| Cell | Data |
486
487Text after.";
488 assert_eq!(fixed, expected);
489 }
490
491 #[test]
492 fn test_fix_multiple_tables() {
493 let rule = MD058BlanksAroundTables::default();
494 let content = "Start
495| T1 | C1 |
496|----|----|
497| D1 | V1 |
498Middle
499| T2 | C2 |
500|----|----|
501| D2 | V2 |
502End";
503 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
504 let fixed = rule.fix(&ctx).unwrap();
505
506 let expected = "Start
507
508| T1 | C1 |
509|----|----|
510| D1 | V1 |
511
512Middle
513
514| T2 | C2 |
515|----|----|
516| D2 | V2 |
517
518End";
519 assert_eq!(fixed, expected);
520 }
521
522 #[test]
523 fn test_empty_content() {
524 let rule = MD058BlanksAroundTables::default();
525 let content = "";
526 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
527 let result = rule.check(&ctx).unwrap();
528
529 assert_eq!(result.len(), 0);
530 }
531
532 #[test]
533 fn test_no_tables() {
534 let rule = MD058BlanksAroundTables::default();
535 let content = "Just regular text.
536No tables here.
537Only paragraphs.";
538 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
539 let result = rule.check(&ctx).unwrap();
540
541 assert_eq!(result.len(), 0);
542 }
543
544 #[test]
545 fn test_code_block_with_table() {
546 let rule = MD058BlanksAroundTables::default();
547 let content = "Text before.
548```
549| Not | A | Table |
550|-----|---|-------|
551| In | Code | Block |
552```
553Text after.";
554 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
555 let result = rule.check(&ctx).unwrap();
556
557 assert_eq!(result.len(), 0);
559 }
560
561 #[test]
562 fn test_table_with_complex_content() {
563 let rule = MD058BlanksAroundTables::default();
564 let content = "# Heading
565| Column 1 | Column 2 | Column 3 |
566|:---------|:--------:|---------:|
567| Left | Center | Right |
568| Data | More | Info |
569## Another Heading";
570 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
571 let result = rule.check(&ctx).unwrap();
572
573 assert_eq!(result.len(), 2);
574 assert!(result[0].message.contains("Missing blank line before table"));
575 assert!(result[1].message.contains("Missing blank line after table"));
576 }
577
578 #[test]
579 fn test_table_with_empty_cells() {
580 let rule = MD058BlanksAroundTables::default();
581 let content = "Text.
582
583| | | |
584|-----|-----|-----|
585| | X | |
586| O | | X |
587
588More text.";
589 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
590 let result = rule.check(&ctx).unwrap();
591
592 assert_eq!(result.len(), 0);
593 }
594
595 #[test]
596 fn test_table_with_unicode() {
597 let rule = MD058BlanksAroundTables::default();
598 let content = "Unicode test.
599| 名前 | 年齢 | 都市 |
600|------|------|------|
601| 田中 | 25 | 東京 |
602| 佐藤 | 30 | 大阪 |
603End.";
604 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
605 let result = rule.check(&ctx).unwrap();
606
607 assert_eq!(result.len(), 2);
608 }
609
610 #[test]
611 fn test_table_with_long_cells() {
612 let rule = MD058BlanksAroundTables::default();
613 let content = "Before.
614
615| Short | Very very very very very very very very long header |
616|-------|-----------------------------------------------------|
617| Data | This is an extremely long cell content that goes on |
618
619After.";
620 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
621 let result = rule.check(&ctx).unwrap();
622
623 assert_eq!(result.len(), 0);
624 }
625
626 #[test]
627 fn test_table_without_content_rows() {
628 let rule = MD058BlanksAroundTables::default();
629 let content = "Text.
630| Header 1 | Header 2 |
631|----------|----------|
632Next paragraph.";
633 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
634 let result = rule.check(&ctx).unwrap();
635
636 assert_eq!(result.len(), 2);
638 }
639
640 #[test]
641 fn test_indented_table() {
642 let rule = MD058BlanksAroundTables::default();
643 let content = "List item:
644
645 | Indented | Table |
646 |----------|-------|
647 | Data | Here |
648
649 More content.";
650 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
651 let result = rule.check(&ctx).unwrap();
652
653 assert_eq!(result.len(), 0);
655 }
656
657 #[test]
658 fn test_single_column_table_not_detected() {
659 let rule = MD058BlanksAroundTables::default();
660 let content = "Text before.
661| Single |
662|--------|
663| Column |
664Text after.";
665 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
666 let result = rule.check(&ctx).unwrap();
667
668 assert_eq!(result.len(), 2);
671 assert!(result[0].message.contains("before"));
672 assert!(result[1].message.contains("after"));
673 }
674
675 #[test]
676 fn test_config_minimum_before() {
677 let config = MD058Config {
678 minimum_before: 2,
679 minimum_after: 1,
680 };
681 let rule = MD058BlanksAroundTables::from_config_struct(config);
682
683 let content = "Text before.
684
685| Header | Col 2 |
686|--------|-------|
687| Cell | Data |
688
689Text after.";
690 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
691 let result = rule.check(&ctx).unwrap();
692
693 assert_eq!(result.len(), 1);
695 assert!(result[0].message.contains("Missing 1 blank lines before table"));
696 }
697
698 #[test]
699 fn test_config_minimum_after() {
700 let config = MD058Config {
701 minimum_before: 1,
702 minimum_after: 3,
703 };
704 let rule = MD058BlanksAroundTables::from_config_struct(config);
705
706 let content = "Text before.
707
708| Header | Col 2 |
709|--------|-------|
710| Cell | Data |
711
712More text.";
713 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
714 let result = rule.check(&ctx).unwrap();
715
716 assert_eq!(result.len(), 1);
718 assert!(result[0].message.contains("Missing 2 blank lines after table"));
719 }
720
721 #[test]
722 fn test_config_both_minimum() {
723 let config = MD058Config {
724 minimum_before: 2,
725 minimum_after: 2,
726 };
727 let rule = MD058BlanksAroundTables::from_config_struct(config);
728
729 let content = "Text before.
730| Header | Col 2 |
731|--------|-------|
732| Cell | Data |
733More text.";
734 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
735 let result = rule.check(&ctx).unwrap();
736
737 assert_eq!(result.len(), 2);
739 assert!(result[0].message.contains("Missing 2 blank lines before table"));
740 assert!(result[1].message.contains("Missing 2 blank lines after table"));
741 }
742
743 #[test]
744 fn test_config_zero_minimum() {
745 let config = MD058Config {
746 minimum_before: 0,
747 minimum_after: 0,
748 };
749 let rule = MD058BlanksAroundTables::from_config_struct(config);
750
751 let content = "Text before.
752| Header | Col 2 |
753|--------|-------|
754| Cell | Data |
755More text.";
756 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
757 let result = rule.check(&ctx).unwrap();
758
759 assert_eq!(result.len(), 0);
761 }
762
763 #[test]
764 fn test_fix_with_custom_config() {
765 let config = MD058Config {
766 minimum_before: 2,
767 minimum_after: 3,
768 };
769 let rule = MD058BlanksAroundTables::from_config_struct(config);
770
771 let content = "Text before.
772| Header | Col 2 |
773|--------|-------|
774| Cell | Data |
775Text after.";
776 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
777 let fixed = rule.fix(&ctx).unwrap();
778
779 let expected = "Text before.
780
781
782| Header | Col 2 |
783|--------|-------|
784| Cell | Data |
785
786
787
788Text after.";
789 assert_eq!(fixed, expected);
790 }
791
792 #[test]
793 fn test_default_config_section() {
794 let rule = MD058BlanksAroundTables::default();
795 let config_section = rule.default_config_section();
796
797 assert!(config_section.is_some());
798 let (name, value) = config_section.unwrap();
799 assert_eq!(name, "MD058");
800
801 if let toml::Value::Table(table) = value {
803 assert!(table.contains_key("minimum-before"));
804 assert!(table.contains_key("minimum-after"));
805 assert_eq!(table["minimum-before"], toml::Value::Integer(1));
806 assert_eq!(table["minimum-after"], toml::Value::Integer(1));
807 } else {
808 panic!("Expected TOML table");
809 }
810 }
811
812 #[test]
813 fn test_blank_lines_counting() {
814 let rule = MD058BlanksAroundTables::default();
815 let lines = vec!["text", "", "", "table", "more", "", "end"];
816
817 assert_eq!(rule.count_blank_lines_before(&lines, 3), 2);
819
820 assert_eq!(rule.count_blank_lines_after(&lines, 4), 1);
822
823 assert_eq!(rule.count_blank_lines_before(&lines, 0), 0);
825
826 assert_eq!(rule.count_blank_lines_after(&lines, 6), 0);
828 }
829
830 #[test]
831 fn test_issue_25_table_with_long_line() {
832 let rule = MD058BlanksAroundTables::default();
834 let content = "# Title\n\nThis is a table:\n\n| Name | Query |\n| ------------- | -------------------------------------------------------- |\n| b | a |\n| c | a |\n| d | a |\n| long | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |\n| e | a |\n| f | a |\n| g | a |";
835 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
836
837 let table_blocks = TableUtils::find_table_blocks(content, &ctx);
839 for (i, block) in table_blocks.iter().enumerate() {
840 eprintln!(
841 "Table {}: start={}, end={}, header={}, delimiter={}, content_lines={:?}",
842 i + 1,
843 block.start_line + 1,
844 block.end_line + 1,
845 block.header_line + 1,
846 block.delimiter_line + 1,
847 block.content_lines.iter().map(|x| x + 1).collect::<Vec<_>>()
848 );
849 }
850
851 let result = rule.check(&ctx).unwrap();
852
853 assert_eq!(table_blocks.len(), 1, "Should detect exactly one table block");
855
856 assert_eq!(result.len(), 0, "Should not flag any MD058 issues for a complete table");
858 }
859
860 #[test]
861 fn test_fix_preserves_blockquote_prefix_before_table() {
862 let rule = MD058BlanksAroundTables::default();
864
865 let content = "> Text before
866> | H1 | H2 |
867> |----|---|
868> | a | b |";
869 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
870 let fixed = rule.fix(&ctx).unwrap();
871
872 let expected = "> Text before
874>
875> | H1 | H2 |
876> |----|---|
877> | a | b |";
878 assert_eq!(
879 fixed, expected,
880 "Fix should insert '>' blank line before table, not plain blank line"
881 );
882 }
883
884 #[test]
885 fn test_fix_preserves_blockquote_prefix_after_table() {
886 let rule = MD058BlanksAroundTables::default();
888
889 let content = "> | H1 | H2 |
890> |----|---|
891> | a | b |
892> Text after";
893 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
894 let fixed = rule.fix(&ctx).unwrap();
895
896 let expected = "> | H1 | H2 |
898> |----|---|
899> | a | b |
900>
901> Text after";
902 assert_eq!(
903 fixed, expected,
904 "Fix should insert '>' blank line after table, not plain blank line"
905 );
906 }
907
908 #[test]
909 fn test_fix_preserves_nested_blockquote_prefix_for_table() {
910 let rule = MD058BlanksAroundTables::default();
912
913 let content = ">> Nested quote
914>> | H1 |
915>> |----|
916>> | a |
917>> More text";
918 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
919 let fixed = rule.fix(&ctx).unwrap();
920
921 let expected = ">> Nested quote
923>>
924>> | H1 |
925>> |----|
926>> | a |
927>>
928>> More text";
929 assert_eq!(fixed, expected, "Fix should preserve nested blockquote prefix '>>'");
930 }
931
932 #[test]
933 fn test_fix_preserves_triple_nested_blockquote_prefix_for_table() {
934 let rule = MD058BlanksAroundTables::default();
936
937 let content = ">>> Triple nested
938>>> | A | B |
939>>> |---|---|
940>>> | 1 | 2 |
941>>> More text";
942 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
943 let fixed = rule.fix(&ctx).unwrap();
944
945 let expected = ">>> Triple nested
946>>>
947>>> | A | B |
948>>> |---|---|
949>>> | 1 | 2 |
950>>>
951>>> More text";
952 assert_eq!(
953 fixed, expected,
954 "Fix should preserve triple-nested blockquote prefix '>>>'"
955 );
956 }
957
958 #[test]
965 fn test_is_blank_line_with_blockquote_continuation() {
966 let rule = MD058BlanksAroundTables::default();
968
969 assert!(rule.is_blank_line(""));
971 assert!(rule.is_blank_line(" "));
972 assert!(rule.is_blank_line("\t"));
973 assert!(rule.is_blank_line(" \t "));
974
975 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(">> "));
981 assert!(rule.is_blank_line(">>>"));
982 assert!(rule.is_blank_line("> > "));
983 assert!(rule.is_blank_line("> > > "));
984 assert!(rule.is_blank_line(" > ")); assert!(!rule.is_blank_line("text"));
988 assert!(!rule.is_blank_line("> text"));
989 assert!(!rule.is_blank_line(">> text"));
990 assert!(!rule.is_blank_line("> | table |"));
991 assert!(!rule.is_blank_line("| table |"));
992 }
993
994 #[test]
995 fn test_issue_305_no_warning_blockquote_with_existing_blank_before_table() {
996 let rule = MD058BlanksAroundTables::default();
999
1000 let content = "> Text before
1001>
1002> | H1 | H2 |
1003> |----|---|
1004> | a | b |";
1005 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1006 let result = rule.check(&ctx).unwrap();
1007
1008 assert_eq!(
1009 result.len(),
1010 0,
1011 "Should not warn when blockquote already has blank line before table"
1012 );
1013 }
1014
1015 #[test]
1016 fn test_issue_305_no_warning_blockquote_with_existing_blank_after_table() {
1017 let rule = MD058BlanksAroundTables::default();
1020
1021 let content = "> | H1 | H2 |
1022> |----|---|
1023> | a | b |
1024>
1025> Text after";
1026 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1027 let result = rule.check(&ctx).unwrap();
1028
1029 assert_eq!(
1030 result.len(),
1031 0,
1032 "Should not warn when blockquote already has blank line after table"
1033 );
1034 }
1035
1036 #[test]
1037 fn test_issue_305_no_warning_blockquote_with_both_blank_lines() {
1038 let rule = MD058BlanksAroundTables::default();
1040
1041 let content = "> The following options are available:
1042>
1043> | Option | Default | Description |
1044> |--------|-----------|-------------------|
1045> | port | 3000 | Server port |
1046> | host | localhost | Server host |";
1047 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1048 let result = rule.check(&ctx).unwrap();
1049
1050 assert_eq!(
1051 result.len(),
1052 0,
1053 "Issue #305: Should not warn for valid table inside blockquote with blank line"
1054 );
1055 }
1056
1057 #[test]
1058 fn test_issue_305_no_warning_nested_blockquote_with_blank_lines() {
1059 let rule = MD058BlanksAroundTables::default();
1061
1062 let content = ">> Nested text
1063>>
1064>> | Col1 | Col2 |
1065>> |------|------|
1066>> | val1 | val2 |
1067>>
1068>> More text";
1069 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1070 let result = rule.check(&ctx).unwrap();
1071
1072 assert_eq!(
1073 result.len(),
1074 0,
1075 "Should not warn for nested blockquote table with blank lines"
1076 );
1077 }
1078
1079 #[test]
1080 fn test_issue_305_no_warning_triple_nested_blockquote_with_blank_lines() {
1081 let rule = MD058BlanksAroundTables::default();
1083
1084 let content = ">>> Deep nesting
1085>>>
1086>>> | A | B |
1087>>> |---|---|
1088>>> | 1 | 2 |
1089>>>
1090>>> End";
1091 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1092 let result = rule.check(&ctx).unwrap();
1093
1094 assert_eq!(
1095 result.len(),
1096 0,
1097 "Should not warn for triple-nested blockquote table with blank lines"
1098 );
1099 }
1100
1101 #[test]
1102 fn test_issue_305_fix_does_not_corrupt_valid_blockquote_table() {
1103 let rule = MD058BlanksAroundTables::default();
1105
1106 let content = "> Text before
1107>
1108> | H1 | H2 |
1109> |----|---|
1110> | a | b |
1111>
1112> Text after";
1113 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1114 let fixed = rule.fix(&ctx).unwrap();
1115
1116 assert_eq!(fixed, content, "Fix should not modify already-valid blockquote table");
1117 }
1118
1119 #[test]
1120 fn test_issue_305_blockquote_blank_with_trailing_space() {
1121 let rule = MD058BlanksAroundTables::default();
1123
1124 let content = "> Text before
1126>
1127> | H1 | H2 |
1128> |----|---|
1129> | a | b |";
1130 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1131 let result = rule.check(&ctx).unwrap();
1132
1133 assert_eq!(
1134 result.len(),
1135 0,
1136 "Should recognize '> ' (with trailing space) as blank line"
1137 );
1138 }
1139
1140 #[test]
1141 fn test_issue_305_spaced_nested_blockquote() {
1142 let rule = MD058BlanksAroundTables::default();
1144
1145 let content = "> > Nested text
1146> >
1147> > | H1 |
1148> > |----|
1149> > | a |";
1150 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1151 let result = rule.check(&ctx).unwrap();
1152
1153 assert_eq!(
1154 result.len(),
1155 0,
1156 "Should recognize '> > ' style nested blockquote blank line"
1157 );
1158 }
1159
1160 #[test]
1161 fn test_mixed_regular_and_blockquote_tables() {
1162 let rule = MD058BlanksAroundTables::default();
1164
1165 let content = "# Mixed Content
1166
1167Regular table:
1168
1169| A | B |
1170|---|---|
1171| 1 | 2 |
1172
1173And a blockquote table:
1174
1175> Quote text
1176>
1177> | X | Y |
1178> |---|---|
1179> | 3 | 4 |
1180>
1181> End quote
1182
1183Final paragraph.";
1184 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1185 let result = rule.check(&ctx).unwrap();
1186
1187 assert_eq!(
1188 result.len(),
1189 0,
1190 "Should handle mixed regular and blockquote tables correctly"
1191 );
1192 }
1193
1194 #[test]
1195 fn test_blockquote_table_at_document_start() {
1196 let rule = MD058BlanksAroundTables::default();
1198
1199 let content = "> | H1 | H2 |
1200> |----|---|
1201> | a | b |
1202>
1203> Text after";
1204 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1205 let result = rule.check(&ctx).unwrap();
1206
1207 assert_eq!(
1208 result.len(),
1209 0,
1210 "Should not require blank line before table at document start (even in blockquote)"
1211 );
1212 }
1213
1214 #[test]
1215 fn test_blockquote_table_at_document_end() {
1216 let rule = MD058BlanksAroundTables::default();
1218
1219 let content = "> Text before
1220>
1221> | H1 | H2 |
1222> |----|---|
1223> | a | b |";
1224 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1225 let result = rule.check(&ctx).unwrap();
1226
1227 assert_eq!(
1228 result.len(),
1229 0,
1230 "Should not require blank line after table at document end"
1231 );
1232 }
1233
1234 #[test]
1235 fn test_blockquote_table_missing_blank_still_detected() {
1236 let rule = MD058BlanksAroundTables::default();
1238
1239 let content = "> Text before
1240> | H1 | H2 |
1241> |----|---|
1242> | a | b |
1243> Text after";
1244 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1245 let result = rule.check(&ctx).unwrap();
1246
1247 assert_eq!(
1249 result.len(),
1250 2,
1251 "Should still detect missing blank lines in blockquote tables"
1252 );
1253 assert!(result[0].message.contains("before table"));
1254 assert!(result[1].message.contains("after table"));
1255 }
1256
1257 #[test]
1258 fn test_blockquote_table_fix_adds_correct_prefix() {
1259 let rule = MD058BlanksAroundTables::default();
1261
1262 let content = "> Text before
1263> | H1 | H2 |
1264> |----|---|
1265> | a | b |
1266> Text after";
1267 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1268 let fixed = rule.fix(&ctx).unwrap();
1269
1270 let expected = "> Text before
1271>
1272> | H1 | H2 |
1273> |----|---|
1274> | a | b |
1275>
1276> Text after";
1277 assert_eq!(fixed, expected, "Fix should add blockquote-prefixed blank lines");
1278 }
1279
1280 #[test]
1281 fn test_multiple_blockquote_tables_with_valid_spacing() {
1282 let rule = MD058BlanksAroundTables::default();
1284
1285 let content = "> First table:
1286>
1287> | A | B |
1288> |---|---|
1289> | 1 | 2 |
1290>
1291> Second table:
1292>
1293> | X | Y |
1294> |---|---|
1295> | 3 | 4 |
1296>
1297> End";
1298 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1299 let result = rule.check(&ctx).unwrap();
1300
1301 assert_eq!(
1302 result.len(),
1303 0,
1304 "Should handle multiple blockquote tables with valid spacing"
1305 );
1306 }
1307
1308 #[test]
1309 fn test_blockquote_table_with_minimum_before_config() {
1310 let config = MD058Config {
1312 minimum_before: 2,
1313 minimum_after: 1,
1314 };
1315 let rule = MD058BlanksAroundTables::from_config_struct(config);
1316
1317 let content = "> Text
1318>
1319> | H1 |
1320> |----|
1321> | a |";
1322 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1323 let result = rule.check(&ctx).unwrap();
1324
1325 assert_eq!(result.len(), 1);
1327 assert!(result[0].message.contains("before table"));
1328 }
1329
1330 #[test]
1346 fn md058_pandoc_grid_tables_not_flagged() {
1347 let rule = MD058BlanksAroundTables::default();
1348 let content = "Some text before.
1351+---+---+
1352| a | b |
1353+===+===+
1354| 1 | 2 |
1355+---+---+
1356Some text after.";
1357
1358 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1360 let result = rule.check(&ctx).unwrap();
1361 assert!(
1362 result.is_empty(),
1363 "MD058 should not flag blank lines around Pandoc grid tables (excluded by table_blocks): {result:?}"
1364 );
1365
1366 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1368 let result_std = rule.check(&ctx_std).unwrap();
1369 assert!(
1370 result_std.is_empty(),
1371 "MD058 should not flag grid-table-like content under Standard: {result_std:?}"
1372 );
1373 }
1374
1375 #[test]
1376 fn md058_pandoc_multi_line_tables_not_flagged() {
1377 let rule = MD058BlanksAroundTables::default();
1378 let content = "Some text.
1379--------- -----------
1380Header 1 Header 2
1381--------- -----------
1382Cell 1 Cell 2
1383--------- -----------
1384More text.";
1385
1386 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1387 let result = rule.check(&ctx).unwrap();
1388 assert!(
1389 result.is_empty(),
1390 "MD058 should not flag Pandoc multi-line tables: {result:?}"
1391 );
1392
1393 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1394 let result_std = rule.check(&ctx_std).unwrap();
1395 assert!(
1396 result_std.is_empty(),
1397 "MD058 should not flag multi-line table content under Standard: {result_std:?}"
1398 );
1399 }
1400
1401 #[test]
1402 fn md058_pandoc_line_blocks_not_flagged() {
1403 let rule = MD058BlanksAroundTables::default();
1404 let content = "Some text.
1406| First line
1407| Second line
1408More text.";
1409
1410 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1411 let result = rule.check(&ctx).unwrap();
1412 assert!(
1413 result.is_empty(),
1414 "MD058 should not treat Pandoc line blocks as tables: {result:?}"
1415 );
1416
1417 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1418 let result_std = rule.check(&ctx_std).unwrap();
1419 assert!(
1420 result_std.is_empty(),
1421 "MD058 should not treat line-block-like content as tables under Standard: {result_std:?}"
1422 );
1423 }
1424
1425 #[test]
1426 fn md058_pandoc_pipe_table_captions_not_flagged() {
1427 let rule = MD058BlanksAroundTables::default();
1428 let content = "\
1431Some text.
1432
1433| H1 | H2 |
1434|----|-----|
1435| a | b |
1436
1437: My table caption
1438More text.";
1439
1440 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1441 let result = rule.check(&ctx).unwrap();
1442 assert!(
1443 result.is_empty(),
1444 "MD058 should not flag the pipe-table caption line as needing blank lines: {result:?}"
1445 );
1446
1447 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1450 let result_std = rule.check(&ctx_std).unwrap();
1451 assert!(
1452 result_std.is_empty(),
1453 "MD058 table with caption — caption not a table row under Standard: {result_std:?}"
1454 );
1455 }
1456
1457 #[test]
1458 fn md058_hugo_block_attribute_after_table_not_flagged() {
1459 let rule = MD058BlanksAroundTables::default();
1462 let content = "\
1463Some text.
1464
1465| H1 | H2 |
1466|----|-----|
1467| a | b |
1468{class=\"table table-striped\"}
1469
1470More text.";
1471
1472 for flavor in [
1473 crate::config::MarkdownFlavor::Hugo,
1474 crate::config::MarkdownFlavor::MkDocs,
1475 crate::config::MarkdownFlavor::Kramdown,
1476 ] {
1477 let ctx = LintContext::new(content, flavor, None);
1478 let result = rule.check(&ctx).unwrap();
1479 assert!(
1480 result.is_empty(),
1481 "MD058 should not flag the block attribute line under {flavor:?}: {result:?}"
1482 );
1483 }
1484
1485 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1488 let result_std = rule.check(&ctx_std).unwrap();
1489 assert_eq!(
1490 result_std.len(),
1491 1,
1492 "MD058 must flag the missing blank after table under Standard: {result_std:?}"
1493 );
1494 assert!(result_std[0].message.contains("Missing blank line after table"));
1495 }
1496
1497 #[test]
1498 fn test_fix_preserves_trailing_newline() {
1499 let rule = MD058BlanksAroundTables::default();
1500
1501 let content = "Intro\n| a | b |\n| --- | --- |\n| 1 | 2 |\nAfter\n";
1502 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1503 let fixed = rule.fix(&ctx).unwrap();
1504
1505 assert!(fixed.ends_with('\n'), "Fix should preserve trailing newline");
1506 assert_eq!(fixed, "Intro\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n\nAfter\n");
1507 }
1508
1509 #[test]
1510 fn test_fix_preserves_no_trailing_newline() {
1511 let rule = MD058BlanksAroundTables::default();
1512
1513 let content = "Intro\n| a | b |\n| --- | --- |\n| 1 | 2 |\nAfter";
1514 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1515 let fixed = rule.fix(&ctx).unwrap();
1516
1517 assert!(
1518 !fixed.ends_with('\n'),
1519 "Fix should not add trailing newline if original didn't have one"
1520 );
1521 assert_eq!(fixed, "Intro\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n\nAfter");
1522 }
1523}