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