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