1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::rule_config_serde::RuleConfig;
3use crate::utils::kramdown_utils::is_kramdown_block_attribute;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[serde(rename_all = "kebab-case")]
15pub struct MD058Config {
16 #[serde(default = "default_minimum_before")]
18 pub minimum_before: usize,
19 #[serde(default = "default_minimum_after")]
21 pub minimum_after: usize,
22}
23
24impl Default for MD058Config {
25 fn default() -> Self {
26 Self {
27 minimum_before: default_minimum_before(),
28 minimum_after: default_minimum_after(),
29 }
30 }
31}
32
33fn default_minimum_before() -> usize {
34 1
35}
36
37fn default_minimum_after() -> usize {
38 1
39}
40
41impl RuleConfig for MD058Config {
42 const RULE_NAME: &'static str = "MD058";
43}
44
45#[derive(Clone, Default)]
46pub struct MD058BlanksAroundTables {
47 config: MD058Config,
48}
49
50impl MD058BlanksAroundTables {
51 pub fn from_config_struct(config: MD058Config) -> Self {
53 Self { config }
54 }
55
56 fn is_blank_line(&self, line: &str) -> bool {
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_kramdown_block_attribute(lines[table_block.end_line + 1])
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].len() + 1,
186 end_line: table_block.end_line + 1,
187 end_column: lines[table_block.end_line].len() + 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 Ok(result.join("\n"))
282 }
283
284 fn as_any(&self) -> &dyn std::any::Any {
285 self
286 }
287
288 crate::impl_rule_config_methods!(MD058Config);
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::lint_context::LintContext;
295 use crate::utils::table_utils::TableUtils;
296
297 #[test]
298 fn test_table_with_blanks() {
299 let rule = MD058BlanksAroundTables::default();
300 let content = "Some text before.
301
302| Header 1 | Header 2 |
303|----------|----------|
304| Cell 1 | Cell 2 |
305
306Some text after.";
307 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
308 let result = rule.check(&ctx).unwrap();
309
310 assert_eq!(result.len(), 0);
311 }
312
313 #[test]
314 fn test_table_missing_blank_before() {
315 let rule = MD058BlanksAroundTables::default();
316 let content = "Some text before.
317| Header 1 | Header 2 |
318|----------|----------|
319| Cell 1 | Cell 2 |
320
321Some text after.";
322 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
323 let result = rule.check(&ctx).unwrap();
324
325 assert_eq!(result.len(), 1);
326 assert_eq!(result[0].line, 2);
327 assert!(result[0].message.contains("Missing blank line before table"));
328 }
329
330 #[test]
331 fn test_table_missing_blank_after() {
332 let rule = MD058BlanksAroundTables::default();
333 let content = "Some text before.
334
335| Header 1 | Header 2 |
336|----------|----------|
337| Cell 1 | Cell 2 |
338Some text after.";
339 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
340 let result = rule.check(&ctx).unwrap();
341
342 assert_eq!(result.len(), 1);
343 assert_eq!(result[0].line, 5);
344 assert!(result[0].message.contains("Missing blank line after table"));
345 }
346
347 #[test]
348 fn test_table_missing_both_blanks() {
349 let rule = MD058BlanksAroundTables::default();
350 let content = "Some text before.
351| Header 1 | Header 2 |
352|----------|----------|
353| Cell 1 | Cell 2 |
354Some text after.";
355 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
356 let result = rule.check(&ctx).unwrap();
357
358 assert_eq!(result.len(), 2);
359 assert!(result[0].message.contains("Missing blank line before table"));
360 assert!(result[1].message.contains("Missing blank line after table"));
361 }
362
363 #[test]
364 fn test_table_at_start_of_document() {
365 let rule = MD058BlanksAroundTables::default();
366 let content = "| Header 1 | Header 2 |
367|----------|----------|
368| Cell 1 | Cell 2 |
369
370Some text after.";
371 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
372 let result = rule.check(&ctx).unwrap();
373
374 assert_eq!(result.len(), 0);
376 }
377
378 #[test]
379 fn test_table_at_end_of_document() {
380 let rule = MD058BlanksAroundTables::default();
381 let content = "Some text before.
382
383| Header 1 | Header 2 |
384|----------|----------|
385| Cell 1 | Cell 2 |";
386 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
387 let result = rule.check(&ctx).unwrap();
388
389 assert_eq!(result.len(), 0);
391 }
392
393 #[test]
394 fn test_multiple_tables() {
395 let rule = MD058BlanksAroundTables::default();
396 let content = "Text before first table.
397| Col 1 | Col 2 |
398|--------|-------|
399| Data 1 | Val 1 |
400Text between tables.
401| Col A | Col B |
402|--------|-------|
403| Data 2 | Val 2 |
404Text after second table.";
405 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
406 let result = rule.check(&ctx).unwrap();
407
408 assert_eq!(result.len(), 4);
409 assert!(result[0].message.contains("Missing blank line before table"));
411 assert!(result[1].message.contains("Missing blank line after table"));
412 assert!(result[2].message.contains("Missing blank line before table"));
414 assert!(result[3].message.contains("Missing blank line after table"));
415 }
416
417 #[test]
418 fn test_consecutive_tables() {
419 let rule = MD058BlanksAroundTables::default();
420 let content = "Some text.
421
422| Col 1 | Col 2 |
423|--------|-------|
424| Data 1 | Val 1 |
425
426| Col A | Col B |
427|--------|-------|
428| Data 2 | Val 2 |
429
430More text.";
431 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
432 let result = rule.check(&ctx).unwrap();
433
434 assert_eq!(result.len(), 0);
436 }
437
438 #[test]
439 fn test_consecutive_tables_no_blank() {
440 let rule = MD058BlanksAroundTables::default();
441 let content = "Some text.
443
444| Col 1 | Col 2 |
445|--------|-------|
446| Data 1 | Val 1 |
447Text between.
448| Col A | Col B |
449|--------|-------|
450| Data 2 | Val 2 |
451
452More text.";
453 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
454 let result = rule.check(&ctx).unwrap();
455
456 assert_eq!(result.len(), 2);
458 assert!(result[0].message.contains("Missing blank line after table"));
459 assert!(result[1].message.contains("Missing blank line before table"));
460 }
461
462 #[test]
463 fn test_fix_missing_blanks() {
464 let rule = MD058BlanksAroundTables::default();
465 let content = "Text before.
466| Header | Col 2 |
467|--------|-------|
468| Cell | Data |
469Text after.";
470 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
471 let fixed = rule.fix(&ctx).unwrap();
472
473 let expected = "Text before.
474
475| Header | Col 2 |
476|--------|-------|
477| Cell | Data |
478
479Text after.";
480 assert_eq!(fixed, expected);
481 }
482
483 #[test]
484 fn test_fix_multiple_tables() {
485 let rule = MD058BlanksAroundTables::default();
486 let content = "Start
487| T1 | C1 |
488|----|----|
489| D1 | V1 |
490Middle
491| T2 | C2 |
492|----|----|
493| D2 | V2 |
494End";
495 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
496 let fixed = rule.fix(&ctx).unwrap();
497
498 let expected = "Start
499
500| T1 | C1 |
501|----|----|
502| D1 | V1 |
503
504Middle
505
506| T2 | C2 |
507|----|----|
508| D2 | V2 |
509
510End";
511 assert_eq!(fixed, expected);
512 }
513
514 #[test]
515 fn test_empty_content() {
516 let rule = MD058BlanksAroundTables::default();
517 let content = "";
518 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
519 let result = rule.check(&ctx).unwrap();
520
521 assert_eq!(result.len(), 0);
522 }
523
524 #[test]
525 fn test_no_tables() {
526 let rule = MD058BlanksAroundTables::default();
527 let content = "Just regular text.
528No tables here.
529Only paragraphs.";
530 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
531 let result = rule.check(&ctx).unwrap();
532
533 assert_eq!(result.len(), 0);
534 }
535
536 #[test]
537 fn test_code_block_with_table() {
538 let rule = MD058BlanksAroundTables::default();
539 let content = "Text before.
540```
541| Not | A | Table |
542|-----|---|-------|
543| In | Code | Block |
544```
545Text after.";
546 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
547 let result = rule.check(&ctx).unwrap();
548
549 assert_eq!(result.len(), 0);
551 }
552
553 #[test]
554 fn test_table_with_complex_content() {
555 let rule = MD058BlanksAroundTables::default();
556 let content = "# Heading
557| Column 1 | Column 2 | Column 3 |
558|:---------|:--------:|---------:|
559| Left | Center | Right |
560| Data | More | Info |
561## Another Heading";
562 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
563 let result = rule.check(&ctx).unwrap();
564
565 assert_eq!(result.len(), 2);
566 assert!(result[0].message.contains("Missing blank line before table"));
567 assert!(result[1].message.contains("Missing blank line after table"));
568 }
569
570 #[test]
571 fn test_table_with_empty_cells() {
572 let rule = MD058BlanksAroundTables::default();
573 let content = "Text.
574
575| | | |
576|-----|-----|-----|
577| | X | |
578| O | | X |
579
580More text.";
581 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
582 let result = rule.check(&ctx).unwrap();
583
584 assert_eq!(result.len(), 0);
585 }
586
587 #[test]
588 fn test_table_with_unicode() {
589 let rule = MD058BlanksAroundTables::default();
590 let content = "Unicode test.
591| 名前 | 年齢 | 都市 |
592|------|------|------|
593| 田中 | 25 | 東京 |
594| 佐藤 | 30 | 大阪 |
595End.";
596 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
597 let result = rule.check(&ctx).unwrap();
598
599 assert_eq!(result.len(), 2);
600 }
601
602 #[test]
603 fn test_table_with_long_cells() {
604 let rule = MD058BlanksAroundTables::default();
605 let content = "Before.
606
607| Short | Very very very very very very very very long header |
608|-------|-----------------------------------------------------|
609| Data | This is an extremely long cell content that goes on |
610
611After.";
612 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
613 let result = rule.check(&ctx).unwrap();
614
615 assert_eq!(result.len(), 0);
616 }
617
618 #[test]
619 fn test_table_without_content_rows() {
620 let rule = MD058BlanksAroundTables::default();
621 let content = "Text.
622| Header 1 | Header 2 |
623|----------|----------|
624Next paragraph.";
625 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
626 let result = rule.check(&ctx).unwrap();
627
628 assert_eq!(result.len(), 2);
630 }
631
632 #[test]
633 fn test_indented_table() {
634 let rule = MD058BlanksAroundTables::default();
635 let content = "List item:
636
637 | Indented | Table |
638 |----------|-------|
639 | Data | Here |
640
641 More content.";
642 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
643 let result = rule.check(&ctx).unwrap();
644
645 assert_eq!(result.len(), 0);
647 }
648
649 #[test]
650 fn test_single_column_table_not_detected() {
651 let rule = MD058BlanksAroundTables::default();
652 let content = "Text before.
653| Single |
654|--------|
655| Column |
656Text after.";
657 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
658 let result = rule.check(&ctx).unwrap();
659
660 assert_eq!(result.len(), 2);
663 assert!(result[0].message.contains("before"));
664 assert!(result[1].message.contains("after"));
665 }
666
667 #[test]
668 fn test_config_minimum_before() {
669 let config = MD058Config {
670 minimum_before: 2,
671 minimum_after: 1,
672 };
673 let rule = MD058BlanksAroundTables::from_config_struct(config);
674
675 let content = "Text before.
676
677| Header | Col 2 |
678|--------|-------|
679| Cell | Data |
680
681Text after.";
682 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
683 let result = rule.check(&ctx).unwrap();
684
685 assert_eq!(result.len(), 1);
687 assert!(result[0].message.contains("Missing 1 blank lines before table"));
688 }
689
690 #[test]
691 fn test_config_minimum_after() {
692 let config = MD058Config {
693 minimum_before: 1,
694 minimum_after: 3,
695 };
696 let rule = MD058BlanksAroundTables::from_config_struct(config);
697
698 let content = "Text before.
699
700| Header | Col 2 |
701|--------|-------|
702| Cell | Data |
703
704More text.";
705 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
706 let result = rule.check(&ctx).unwrap();
707
708 assert_eq!(result.len(), 1);
710 assert!(result[0].message.contains("Missing 2 blank lines after table"));
711 }
712
713 #[test]
714 fn test_config_both_minimum() {
715 let config = MD058Config {
716 minimum_before: 2,
717 minimum_after: 2,
718 };
719 let rule = MD058BlanksAroundTables::from_config_struct(config);
720
721 let content = "Text before.
722| Header | Col 2 |
723|--------|-------|
724| Cell | Data |
725More text.";
726 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
727 let result = rule.check(&ctx).unwrap();
728
729 assert_eq!(result.len(), 2);
731 assert!(result[0].message.contains("Missing 2 blank lines before table"));
732 assert!(result[1].message.contains("Missing 2 blank lines after table"));
733 }
734
735 #[test]
736 fn test_config_zero_minimum() {
737 let config = MD058Config {
738 minimum_before: 0,
739 minimum_after: 0,
740 };
741 let rule = MD058BlanksAroundTables::from_config_struct(config);
742
743 let content = "Text before.
744| Header | Col 2 |
745|--------|-------|
746| Cell | Data |
747More text.";
748 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
749 let result = rule.check(&ctx).unwrap();
750
751 assert_eq!(result.len(), 0);
753 }
754
755 #[test]
756 fn test_fix_with_custom_config() {
757 let config = MD058Config {
758 minimum_before: 2,
759 minimum_after: 3,
760 };
761 let rule = MD058BlanksAroundTables::from_config_struct(config);
762
763 let content = "Text before.
764| Header | Col 2 |
765|--------|-------|
766| Cell | Data |
767Text after.";
768 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
769 let fixed = rule.fix(&ctx).unwrap();
770
771 let expected = "Text before.
772
773
774| Header | Col 2 |
775|--------|-------|
776| Cell | Data |
777
778
779
780Text after.";
781 assert_eq!(fixed, expected);
782 }
783
784 #[test]
785 fn test_default_config_section() {
786 let rule = MD058BlanksAroundTables::default();
787 let config_section = rule.default_config_section();
788
789 assert!(config_section.is_some());
790 let (name, value) = config_section.unwrap();
791 assert_eq!(name, "MD058");
792
793 if let toml::Value::Table(table) = value {
795 assert!(table.contains_key("minimum-before"));
796 assert!(table.contains_key("minimum-after"));
797 assert_eq!(table["minimum-before"], toml::Value::Integer(1));
798 assert_eq!(table["minimum-after"], toml::Value::Integer(1));
799 } else {
800 panic!("Expected TOML table");
801 }
802 }
803
804 #[test]
805 fn test_blank_lines_counting() {
806 let rule = MD058BlanksAroundTables::default();
807 let lines = vec!["text", "", "", "table", "more", "", "end"];
808
809 assert_eq!(rule.count_blank_lines_before(&lines, 3), 2);
811
812 assert_eq!(rule.count_blank_lines_after(&lines, 4), 1);
814
815 assert_eq!(rule.count_blank_lines_before(&lines, 0), 0);
817
818 assert_eq!(rule.count_blank_lines_after(&lines, 6), 0);
820 }
821
822 #[test]
823 fn test_issue_25_table_with_long_line() {
824 let rule = MD058BlanksAroundTables::default();
826 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 |";
827 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
828
829 let table_blocks = TableUtils::find_table_blocks(content, &ctx);
831 for (i, block) in table_blocks.iter().enumerate() {
832 eprintln!(
833 "Table {}: start={}, end={}, header={}, delimiter={}, content_lines={:?}",
834 i + 1,
835 block.start_line + 1,
836 block.end_line + 1,
837 block.header_line + 1,
838 block.delimiter_line + 1,
839 block.content_lines.iter().map(|x| x + 1).collect::<Vec<_>>()
840 );
841 }
842
843 let result = rule.check(&ctx).unwrap();
844
845 assert_eq!(table_blocks.len(), 1, "Should detect exactly one table block");
847
848 assert_eq!(result.len(), 0, "Should not flag any MD058 issues for a complete table");
850 }
851
852 #[test]
853 fn test_fix_preserves_blockquote_prefix_before_table() {
854 let rule = MD058BlanksAroundTables::default();
856
857 let content = "> Text before
858> | H1 | H2 |
859> |----|---|
860> | a | b |";
861 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
862 let fixed = rule.fix(&ctx).unwrap();
863
864 let expected = "> Text before
866>
867> | H1 | H2 |
868> |----|---|
869> | a | b |";
870 assert_eq!(
871 fixed, expected,
872 "Fix should insert '>' blank line before table, not plain blank line"
873 );
874 }
875
876 #[test]
877 fn test_fix_preserves_blockquote_prefix_after_table() {
878 let rule = MD058BlanksAroundTables::default();
880
881 let content = "> | H1 | H2 |
882> |----|---|
883> | a | b |
884> Text after";
885 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
886 let fixed = rule.fix(&ctx).unwrap();
887
888 let expected = "> | H1 | H2 |
890> |----|---|
891> | a | b |
892>
893> Text after";
894 assert_eq!(
895 fixed, expected,
896 "Fix should insert '>' blank line after table, not plain blank line"
897 );
898 }
899
900 #[test]
901 fn test_fix_preserves_nested_blockquote_prefix_for_table() {
902 let rule = MD058BlanksAroundTables::default();
904
905 let content = ">> Nested quote
906>> | H1 |
907>> |----|
908>> | a |
909>> More text";
910 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
911 let fixed = rule.fix(&ctx).unwrap();
912
913 let expected = ">> Nested quote
915>>
916>> | H1 |
917>> |----|
918>> | a |
919>>
920>> More text";
921 assert_eq!(fixed, expected, "Fix should preserve nested blockquote prefix '>>'");
922 }
923
924 #[test]
925 fn test_fix_preserves_triple_nested_blockquote_prefix_for_table() {
926 let rule = MD058BlanksAroundTables::default();
928
929 let content = ">>> Triple nested
930>>> | A | B |
931>>> |---|---|
932>>> | 1 | 2 |
933>>> More text";
934 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
935 let fixed = rule.fix(&ctx).unwrap();
936
937 let expected = ">>> Triple nested
938>>>
939>>> | A | B |
940>>> |---|---|
941>>> | 1 | 2 |
942>>>
943>>> More text";
944 assert_eq!(
945 fixed, expected,
946 "Fix should preserve triple-nested blockquote prefix '>>>'"
947 );
948 }
949
950 #[test]
957 fn test_is_blank_line_with_blockquote_continuation() {
958 let rule = MD058BlanksAroundTables::default();
960
961 assert!(rule.is_blank_line(""));
963 assert!(rule.is_blank_line(" "));
964 assert!(rule.is_blank_line("\t"));
965 assert!(rule.is_blank_line(" \t "));
966
967 assert!(rule.is_blank_line(">"));
969 assert!(rule.is_blank_line("> "));
970 assert!(rule.is_blank_line("> "));
971 assert!(rule.is_blank_line(">>"));
972 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(" > ")); assert!(!rule.is_blank_line("text"));
980 assert!(!rule.is_blank_line("> text"));
981 assert!(!rule.is_blank_line(">> text"));
982 assert!(!rule.is_blank_line("> | table |"));
983 assert!(!rule.is_blank_line("| table |"));
984 }
985
986 #[test]
987 fn test_issue_305_no_warning_blockquote_with_existing_blank_before_table() {
988 let rule = MD058BlanksAroundTables::default();
991
992 let content = "> Text before
993>
994> | H1 | H2 |
995> |----|---|
996> | a | b |";
997 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
998 let result = rule.check(&ctx).unwrap();
999
1000 assert_eq!(
1001 result.len(),
1002 0,
1003 "Should not warn when blockquote already has blank line before table"
1004 );
1005 }
1006
1007 #[test]
1008 fn test_issue_305_no_warning_blockquote_with_existing_blank_after_table() {
1009 let rule = MD058BlanksAroundTables::default();
1012
1013 let content = "> | H1 | H2 |
1014> |----|---|
1015> | a | b |
1016>
1017> Text after";
1018 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1019 let result = rule.check(&ctx).unwrap();
1020
1021 assert_eq!(
1022 result.len(),
1023 0,
1024 "Should not warn when blockquote already has blank line after table"
1025 );
1026 }
1027
1028 #[test]
1029 fn test_issue_305_no_warning_blockquote_with_both_blank_lines() {
1030 let rule = MD058BlanksAroundTables::default();
1032
1033 let content = "> The following options are available:
1034>
1035> | Option | Default | Description |
1036> |--------|-----------|-------------------|
1037> | port | 3000 | Server port |
1038> | host | localhost | Server host |";
1039 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1040 let result = rule.check(&ctx).unwrap();
1041
1042 assert_eq!(
1043 result.len(),
1044 0,
1045 "Issue #305: Should not warn for valid table inside blockquote with blank line"
1046 );
1047 }
1048
1049 #[test]
1050 fn test_issue_305_no_warning_nested_blockquote_with_blank_lines() {
1051 let rule = MD058BlanksAroundTables::default();
1053
1054 let content = ">> Nested text
1055>>
1056>> | Col1 | Col2 |
1057>> |------|------|
1058>> | val1 | val2 |
1059>>
1060>> More text";
1061 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1062 let result = rule.check(&ctx).unwrap();
1063
1064 assert_eq!(
1065 result.len(),
1066 0,
1067 "Should not warn for nested blockquote table with blank lines"
1068 );
1069 }
1070
1071 #[test]
1072 fn test_issue_305_no_warning_triple_nested_blockquote_with_blank_lines() {
1073 let rule = MD058BlanksAroundTables::default();
1075
1076 let content = ">>> Deep nesting
1077>>>
1078>>> | A | B |
1079>>> |---|---|
1080>>> | 1 | 2 |
1081>>>
1082>>> End";
1083 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1084 let result = rule.check(&ctx).unwrap();
1085
1086 assert_eq!(
1087 result.len(),
1088 0,
1089 "Should not warn for triple-nested blockquote table with blank lines"
1090 );
1091 }
1092
1093 #[test]
1094 fn test_issue_305_fix_does_not_corrupt_valid_blockquote_table() {
1095 let rule = MD058BlanksAroundTables::default();
1097
1098 let content = "> Text before
1099>
1100> | H1 | H2 |
1101> |----|---|
1102> | a | b |
1103>
1104> Text after";
1105 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1106 let fixed = rule.fix(&ctx).unwrap();
1107
1108 assert_eq!(fixed, content, "Fix should not modify already-valid blockquote table");
1109 }
1110
1111 #[test]
1112 fn test_issue_305_blockquote_blank_with_trailing_space() {
1113 let rule = MD058BlanksAroundTables::default();
1115
1116 let content = "> Text before
1118>
1119> | H1 | H2 |
1120> |----|---|
1121> | a | b |";
1122 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1123 let result = rule.check(&ctx).unwrap();
1124
1125 assert_eq!(
1126 result.len(),
1127 0,
1128 "Should recognize '> ' (with trailing space) as blank line"
1129 );
1130 }
1131
1132 #[test]
1133 fn test_issue_305_spaced_nested_blockquote() {
1134 let rule = MD058BlanksAroundTables::default();
1136
1137 let content = "> > Nested text
1138> >
1139> > | H1 |
1140> > |----|
1141> > | a |";
1142 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1143 let result = rule.check(&ctx).unwrap();
1144
1145 assert_eq!(
1146 result.len(),
1147 0,
1148 "Should recognize '> > ' style nested blockquote blank line"
1149 );
1150 }
1151
1152 #[test]
1153 fn test_mixed_regular_and_blockquote_tables() {
1154 let rule = MD058BlanksAroundTables::default();
1156
1157 let content = "# Mixed Content
1158
1159Regular table:
1160
1161| A | B |
1162|---|---|
1163| 1 | 2 |
1164
1165And a blockquote table:
1166
1167> Quote text
1168>
1169> | X | Y |
1170> |---|---|
1171> | 3 | 4 |
1172>
1173> End quote
1174
1175Final paragraph.";
1176 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1177 let result = rule.check(&ctx).unwrap();
1178
1179 assert_eq!(
1180 result.len(),
1181 0,
1182 "Should handle mixed regular and blockquote tables correctly"
1183 );
1184 }
1185
1186 #[test]
1187 fn test_blockquote_table_at_document_start() {
1188 let rule = MD058BlanksAroundTables::default();
1190
1191 let content = "> | H1 | H2 |
1192> |----|---|
1193> | a | b |
1194>
1195> Text after";
1196 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1197 let result = rule.check(&ctx).unwrap();
1198
1199 assert_eq!(
1200 result.len(),
1201 0,
1202 "Should not require blank line before table at document start (even in blockquote)"
1203 );
1204 }
1205
1206 #[test]
1207 fn test_blockquote_table_at_document_end() {
1208 let rule = MD058BlanksAroundTables::default();
1210
1211 let content = "> Text before
1212>
1213> | H1 | H2 |
1214> |----|---|
1215> | a | b |";
1216 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1217 let result = rule.check(&ctx).unwrap();
1218
1219 assert_eq!(
1220 result.len(),
1221 0,
1222 "Should not require blank line after table at document end"
1223 );
1224 }
1225
1226 #[test]
1227 fn test_blockquote_table_missing_blank_still_detected() {
1228 let rule = MD058BlanksAroundTables::default();
1230
1231 let content = "> Text before
1232> | H1 | H2 |
1233> |----|---|
1234> | a | b |
1235> Text after";
1236 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1237 let result = rule.check(&ctx).unwrap();
1238
1239 assert_eq!(
1241 result.len(),
1242 2,
1243 "Should still detect missing blank lines in blockquote tables"
1244 );
1245 assert!(result[0].message.contains("before table"));
1246 assert!(result[1].message.contains("after table"));
1247 }
1248
1249 #[test]
1250 fn test_blockquote_table_fix_adds_correct_prefix() {
1251 let rule = MD058BlanksAroundTables::default();
1253
1254 let content = "> Text before
1255> | H1 | H2 |
1256> |----|---|
1257> | a | b |
1258> Text after";
1259 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1260 let fixed = rule.fix(&ctx).unwrap();
1261
1262 let expected = "> Text before
1263>
1264> | H1 | H2 |
1265> |----|---|
1266> | a | b |
1267>
1268> Text after";
1269 assert_eq!(fixed, expected, "Fix should add blockquote-prefixed blank lines");
1270 }
1271
1272 #[test]
1273 fn test_multiple_blockquote_tables_with_valid_spacing() {
1274 let rule = MD058BlanksAroundTables::default();
1276
1277 let content = "> First table:
1278>
1279> | A | B |
1280> |---|---|
1281> | 1 | 2 |
1282>
1283> Second table:
1284>
1285> | X | Y |
1286> |---|---|
1287> | 3 | 4 |
1288>
1289> End";
1290 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1291 let result = rule.check(&ctx).unwrap();
1292
1293 assert_eq!(
1294 result.len(),
1295 0,
1296 "Should handle multiple blockquote tables with valid spacing"
1297 );
1298 }
1299
1300 #[test]
1301 fn test_blockquote_table_with_minimum_before_config() {
1302 let config = MD058Config {
1304 minimum_before: 2,
1305 minimum_after: 1,
1306 };
1307 let rule = MD058BlanksAroundTables::from_config_struct(config);
1308
1309 let content = "> Text
1310>
1311> | H1 |
1312> |----|
1313> | a |";
1314 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1315 let result = rule.check(&ctx).unwrap();
1316
1317 assert_eq!(result.len(), 1);
1319 assert!(result[0].message.contains("before table"));
1320 }
1321
1322 #[test]
1338 fn md058_pandoc_grid_tables_not_flagged() {
1339 let rule = MD058BlanksAroundTables::default();
1340 let content = "Some text before.
1343+---+---+
1344| a | b |
1345+===+===+
1346| 1 | 2 |
1347+---+---+
1348Some text after.";
1349
1350 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1352 let result = rule.check(&ctx).unwrap();
1353 assert!(
1354 result.is_empty(),
1355 "MD058 should not flag blank lines around Pandoc grid tables (excluded by table_blocks): {result:?}"
1356 );
1357
1358 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1360 let result_std = rule.check(&ctx_std).unwrap();
1361 assert!(
1362 result_std.is_empty(),
1363 "MD058 should not flag grid-table-like content under Standard: {result_std:?}"
1364 );
1365 }
1366
1367 #[test]
1368 fn md058_pandoc_multi_line_tables_not_flagged() {
1369 let rule = MD058BlanksAroundTables::default();
1370 let content = "Some text.
1371--------- -----------
1372Header 1 Header 2
1373--------- -----------
1374Cell 1 Cell 2
1375--------- -----------
1376More text.";
1377
1378 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1379 let result = rule.check(&ctx).unwrap();
1380 assert!(
1381 result.is_empty(),
1382 "MD058 should not flag Pandoc multi-line tables: {result:?}"
1383 );
1384
1385 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1386 let result_std = rule.check(&ctx_std).unwrap();
1387 assert!(
1388 result_std.is_empty(),
1389 "MD058 should not flag multi-line table content under Standard: {result_std:?}"
1390 );
1391 }
1392
1393 #[test]
1394 fn md058_pandoc_line_blocks_not_flagged() {
1395 let rule = MD058BlanksAroundTables::default();
1396 let content = "Some text.
1398| First line
1399| Second line
1400More text.";
1401
1402 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1403 let result = rule.check(&ctx).unwrap();
1404 assert!(
1405 result.is_empty(),
1406 "MD058 should not treat Pandoc line blocks as tables: {result:?}"
1407 );
1408
1409 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1410 let result_std = rule.check(&ctx_std).unwrap();
1411 assert!(
1412 result_std.is_empty(),
1413 "MD058 should not treat line-block-like content as tables under Standard: {result_std:?}"
1414 );
1415 }
1416
1417 #[test]
1418 fn md058_pandoc_pipe_table_captions_not_flagged() {
1419 let rule = MD058BlanksAroundTables::default();
1420 let content = "\
1423Some text.
1424
1425| H1 | H2 |
1426|----|-----|
1427| a | b |
1428
1429: My table caption
1430More text.";
1431
1432 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1433 let result = rule.check(&ctx).unwrap();
1434 assert!(
1435 result.is_empty(),
1436 "MD058 should not flag the pipe-table caption line as needing blank lines: {result:?}"
1437 );
1438
1439 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1442 let result_std = rule.check(&ctx_std).unwrap();
1443 assert!(
1444 result_std.is_empty(),
1445 "MD058 table with caption — caption not a table row under Standard: {result_std:?}"
1446 );
1447 }
1448}