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 {
152 range: line_index.line_col_to_byte_range(table_block.start_line + 1, 1),
154 replacement,
155 }),
156 });
157 }
158 }
159
160 if table_block.end_line < lines.len() - 1 {
162 let next_line_is_attribute = if table_block.end_line + 1 < lines.len() {
164 is_kramdown_block_attribute(lines[table_block.end_line + 1])
165 } else {
166 false
167 };
168
169 if !next_line_is_attribute {
171 let blank_lines_after = self.count_blank_lines_after(lines, table_block.end_line);
172 if blank_lines_after < self.config.minimum_after {
173 let needed = self.config.minimum_after - blank_lines_after;
174 let message = if self.config.minimum_after == 1 {
175 "Missing blank line after table".to_string()
176 } else {
177 format!("Missing {needed} blank lines after table")
178 };
179
180 let bq_prefix = ctx.blockquote_prefix_for_blank_line(table_block.end_line);
181 let replacement = format!("{bq_prefix}\n").repeat(needed);
182 warnings.push(LintWarning {
183 rule_name: Some(self.name().to_string()),
184 message,
185 line: table_block.end_line + 1,
186 column: lines[table_block.end_line].len() + 1,
187 end_line: table_block.end_line + 1,
188 end_column: lines[table_block.end_line].len() + 2,
189 severity: Severity::Warning,
190 fix: Some(Fix {
191 range: line_index.line_col_to_byte_range(
193 table_block.end_line + 1,
194 lines[table_block.end_line].len() + 1,
195 ),
196 replacement,
197 }),
198 });
199 }
200 }
201 }
202 }
203
204 Ok(warnings)
205 }
206
207 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
208 let content = ctx.content;
209
210 let warnings = self.check(ctx)?;
211 let mut warnings =
212 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
213 if warnings.is_empty() {
214 return Ok(content.to_string());
215 }
216
217 let lines = ctx.raw_lines();
218 let mut result = Vec::new();
219 let mut i = 0;
220
221 while i < lines.len() {
222 let warning_before = warnings
224 .iter()
225 .position(|w| w.line == i + 1 && w.message.contains("before table"));
226
227 if let Some(idx) = warning_before {
228 let warning = &warnings[idx];
229 let needed_blanks = if warning.message.contains("Missing blank line before") {
231 1
232 } else if let Some(start) = warning.message.find("Missing ") {
233 if let Some(end) = warning.message.find(" blank lines before") {
234 warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
235 } else {
236 1
237 }
238 } else {
239 1
240 };
241
242 let bq_prefix = ctx.blockquote_prefix_for_blank_line(i);
244 for _ in 0..needed_blanks {
245 result.push(bq_prefix.clone());
246 }
247 warnings.remove(idx);
248 }
249
250 result.push(lines[i].to_string());
251
252 let warning_after = warnings
254 .iter()
255 .position(|w| w.line == i + 1 && w.message.contains("after table"));
256
257 if let Some(idx) = warning_after {
258 let warning = &warnings[idx];
259 let needed_blanks = if warning.message.contains("Missing blank line after") {
261 1
262 } else if let Some(start) = warning.message.find("Missing ") {
263 if let Some(end) = warning.message.find(" blank lines after") {
264 warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
265 } else {
266 1
267 }
268 } else {
269 1
270 };
271
272 let bq_prefix = ctx.blockquote_prefix_for_blank_line(i);
274 for _ in 0..needed_blanks {
275 result.push(bq_prefix.clone());
276 }
277 warnings.remove(idx);
278 }
279
280 i += 1;
281 }
282
283 Ok(result.join("\n"))
284 }
285
286 fn as_any(&self) -> &dyn std::any::Any {
287 self
288 }
289
290 fn default_config_section(&self) -> Option<(String, toml::Value)> {
291 let default_config = MD058Config::default();
292 let json_value = serde_json::to_value(&default_config).ok()?;
293 let toml_value = crate::rule_config_serde::json_to_toml_value(&json_value)?;
294 if let toml::Value::Table(table) = toml_value {
295 if !table.is_empty() {
296 Some((MD058Config::RULE_NAME.to_string(), toml::Value::Table(table)))
297 } else {
298 None
299 }
300 } else {
301 None
302 }
303 }
304
305 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
306 where
307 Self: Sized,
308 {
309 let rule_config = crate::rule_config_serde::load_rule_config::<MD058Config>(config);
310 Box::new(MD058BlanksAroundTables::from_config_struct(rule_config))
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::lint_context::LintContext;
318 use crate::utils::table_utils::TableUtils;
319
320 #[test]
321 fn test_table_with_blanks() {
322 let rule = MD058BlanksAroundTables::default();
323 let content = "Some text before.
324
325| Header 1 | Header 2 |
326|----------|----------|
327| Cell 1 | Cell 2 |
328
329Some text after.";
330 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
331 let result = rule.check(&ctx).unwrap();
332
333 assert_eq!(result.len(), 0);
334 }
335
336 #[test]
337 fn test_table_missing_blank_before() {
338 let rule = MD058BlanksAroundTables::default();
339 let content = "Some text before.
340| Header 1 | Header 2 |
341|----------|----------|
342| Cell 1 | Cell 2 |
343
344Some text after.";
345 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
346 let result = rule.check(&ctx).unwrap();
347
348 assert_eq!(result.len(), 1);
349 assert_eq!(result[0].line, 2);
350 assert!(result[0].message.contains("Missing blank line before table"));
351 }
352
353 #[test]
354 fn test_table_missing_blank_after() {
355 let rule = MD058BlanksAroundTables::default();
356 let content = "Some text before.
357
358| Header 1 | Header 2 |
359|----------|----------|
360| Cell 1 | Cell 2 |
361Some text after.";
362 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
363 let result = rule.check(&ctx).unwrap();
364
365 assert_eq!(result.len(), 1);
366 assert_eq!(result[0].line, 5);
367 assert!(result[0].message.contains("Missing blank line after table"));
368 }
369
370 #[test]
371 fn test_table_missing_both_blanks() {
372 let rule = MD058BlanksAroundTables::default();
373 let content = "Some text before.
374| Header 1 | Header 2 |
375|----------|----------|
376| Cell 1 | Cell 2 |
377Some text after.";
378 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
379 let result = rule.check(&ctx).unwrap();
380
381 assert_eq!(result.len(), 2);
382 assert!(result[0].message.contains("Missing blank line before table"));
383 assert!(result[1].message.contains("Missing blank line after table"));
384 }
385
386 #[test]
387 fn test_table_at_start_of_document() {
388 let rule = MD058BlanksAroundTables::default();
389 let content = "| Header 1 | Header 2 |
390|----------|----------|
391| Cell 1 | Cell 2 |
392
393Some text after.";
394 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
395 let result = rule.check(&ctx).unwrap();
396
397 assert_eq!(result.len(), 0);
399 }
400
401 #[test]
402 fn test_table_at_end_of_document() {
403 let rule = MD058BlanksAroundTables::default();
404 let content = "Some text before.
405
406| Header 1 | Header 2 |
407|----------|----------|
408| Cell 1 | Cell 2 |";
409 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
410 let result = rule.check(&ctx).unwrap();
411
412 assert_eq!(result.len(), 0);
414 }
415
416 #[test]
417 fn test_multiple_tables() {
418 let rule = MD058BlanksAroundTables::default();
419 let content = "Text before first table.
420| Col 1 | Col 2 |
421|--------|-------|
422| Data 1 | Val 1 |
423Text between tables.
424| Col A | Col B |
425|--------|-------|
426| Data 2 | Val 2 |
427Text after second table.";
428 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
429 let result = rule.check(&ctx).unwrap();
430
431 assert_eq!(result.len(), 4);
432 assert!(result[0].message.contains("Missing blank line before table"));
434 assert!(result[1].message.contains("Missing blank line after table"));
435 assert!(result[2].message.contains("Missing blank line before table"));
437 assert!(result[3].message.contains("Missing blank line after table"));
438 }
439
440 #[test]
441 fn test_consecutive_tables() {
442 let rule = MD058BlanksAroundTables::default();
443 let content = "Some text.
444
445| Col 1 | Col 2 |
446|--------|-------|
447| Data 1 | Val 1 |
448
449| Col A | Col B |
450|--------|-------|
451| Data 2 | Val 2 |
452
453More text.";
454 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
455 let result = rule.check(&ctx).unwrap();
456
457 assert_eq!(result.len(), 0);
459 }
460
461 #[test]
462 fn test_consecutive_tables_no_blank() {
463 let rule = MD058BlanksAroundTables::default();
464 let content = "Some text.
466
467| Col 1 | Col 2 |
468|--------|-------|
469| Data 1 | Val 1 |
470Text between.
471| Col A | Col B |
472|--------|-------|
473| Data 2 | Val 2 |
474
475More text.";
476 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
477 let result = rule.check(&ctx).unwrap();
478
479 assert_eq!(result.len(), 2);
481 assert!(result[0].message.contains("Missing blank line after table"));
482 assert!(result[1].message.contains("Missing blank line before table"));
483 }
484
485 #[test]
486 fn test_fix_missing_blanks() {
487 let rule = MD058BlanksAroundTables::default();
488 let content = "Text before.
489| Header | Col 2 |
490|--------|-------|
491| Cell | Data |
492Text after.";
493 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
494 let fixed = rule.fix(&ctx).unwrap();
495
496 let expected = "Text before.
497
498| Header | Col 2 |
499|--------|-------|
500| Cell | Data |
501
502Text after.";
503 assert_eq!(fixed, expected);
504 }
505
506 #[test]
507 fn test_fix_multiple_tables() {
508 let rule = MD058BlanksAroundTables::default();
509 let content = "Start
510| T1 | C1 |
511|----|----|
512| D1 | V1 |
513Middle
514| T2 | C2 |
515|----|----|
516| D2 | V2 |
517End";
518 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
519 let fixed = rule.fix(&ctx).unwrap();
520
521 let expected = "Start
522
523| T1 | C1 |
524|----|----|
525| D1 | V1 |
526
527Middle
528
529| T2 | C2 |
530|----|----|
531| D2 | V2 |
532
533End";
534 assert_eq!(fixed, expected);
535 }
536
537 #[test]
538 fn test_empty_content() {
539 let rule = MD058BlanksAroundTables::default();
540 let content = "";
541 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
542 let result = rule.check(&ctx).unwrap();
543
544 assert_eq!(result.len(), 0);
545 }
546
547 #[test]
548 fn test_no_tables() {
549 let rule = MD058BlanksAroundTables::default();
550 let content = "Just regular text.
551No tables here.
552Only paragraphs.";
553 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
554 let result = rule.check(&ctx).unwrap();
555
556 assert_eq!(result.len(), 0);
557 }
558
559 #[test]
560 fn test_code_block_with_table() {
561 let rule = MD058BlanksAroundTables::default();
562 let content = "Text before.
563```
564| Not | A | Table |
565|-----|---|-------|
566| In | Code | Block |
567```
568Text after.";
569 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
570 let result = rule.check(&ctx).unwrap();
571
572 assert_eq!(result.len(), 0);
574 }
575
576 #[test]
577 fn test_table_with_complex_content() {
578 let rule = MD058BlanksAroundTables::default();
579 let content = "# Heading
580| Column 1 | Column 2 | Column 3 |
581|:---------|:--------:|---------:|
582| Left | Center | Right |
583| Data | More | Info |
584## Another Heading";
585 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
586 let result = rule.check(&ctx).unwrap();
587
588 assert_eq!(result.len(), 2);
589 assert!(result[0].message.contains("Missing blank line before table"));
590 assert!(result[1].message.contains("Missing blank line after table"));
591 }
592
593 #[test]
594 fn test_table_with_empty_cells() {
595 let rule = MD058BlanksAroundTables::default();
596 let content = "Text.
597
598| | | |
599|-----|-----|-----|
600| | X | |
601| O | | X |
602
603More text.";
604 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
605 let result = rule.check(&ctx).unwrap();
606
607 assert_eq!(result.len(), 0);
608 }
609
610 #[test]
611 fn test_table_with_unicode() {
612 let rule = MD058BlanksAroundTables::default();
613 let content = "Unicode test.
614| 名前 | 年齢 | 都市 |
615|------|------|------|
616| 田中 | 25 | 東京 |
617| 佐藤 | 30 | 大阪 |
618End.";
619 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
620 let result = rule.check(&ctx).unwrap();
621
622 assert_eq!(result.len(), 2);
623 }
624
625 #[test]
626 fn test_table_with_long_cells() {
627 let rule = MD058BlanksAroundTables::default();
628 let content = "Before.
629
630| Short | Very very very very very very very very long header |
631|-------|-----------------------------------------------------|
632| Data | This is an extremely long cell content that goes on |
633
634After.";
635 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
636 let result = rule.check(&ctx).unwrap();
637
638 assert_eq!(result.len(), 0);
639 }
640
641 #[test]
642 fn test_table_without_content_rows() {
643 let rule = MD058BlanksAroundTables::default();
644 let content = "Text.
645| Header 1 | Header 2 |
646|----------|----------|
647Next paragraph.";
648 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
649 let result = rule.check(&ctx).unwrap();
650
651 assert_eq!(result.len(), 2);
653 }
654
655 #[test]
656 fn test_indented_table() {
657 let rule = MD058BlanksAroundTables::default();
658 let content = "List item:
659
660 | Indented | Table |
661 |----------|-------|
662 | Data | Here |
663
664 More content.";
665 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
666 let result = rule.check(&ctx).unwrap();
667
668 assert_eq!(result.len(), 0);
670 }
671
672 #[test]
673 fn test_single_column_table_not_detected() {
674 let rule = MD058BlanksAroundTables::default();
675 let content = "Text before.
676| Single |
677|--------|
678| Column |
679Text after.";
680 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
681 let result = rule.check(&ctx).unwrap();
682
683 assert_eq!(result.len(), 2);
686 assert!(result[0].message.contains("before"));
687 assert!(result[1].message.contains("after"));
688 }
689
690 #[test]
691 fn test_config_minimum_before() {
692 let config = MD058Config {
693 minimum_before: 2,
694 minimum_after: 1,
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
704Text after.";
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 1 blank lines before table"));
711 }
712
713 #[test]
714 fn test_config_minimum_after() {
715 let config = MD058Config {
716 minimum_before: 1,
717 minimum_after: 3,
718 };
719 let rule = MD058BlanksAroundTables::from_config_struct(config);
720
721 let content = "Text before.
722
723| Header | Col 2 |
724|--------|-------|
725| Cell | Data |
726
727More text.";
728 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
729 let result = rule.check(&ctx).unwrap();
730
731 assert_eq!(result.len(), 1);
733 assert!(result[0].message.contains("Missing 2 blank lines after table"));
734 }
735
736 #[test]
737 fn test_config_both_minimum() {
738 let config = MD058Config {
739 minimum_before: 2,
740 minimum_after: 2,
741 };
742 let rule = MD058BlanksAroundTables::from_config_struct(config);
743
744 let content = "Text before.
745| Header | Col 2 |
746|--------|-------|
747| Cell | Data |
748More text.";
749 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
750 let result = rule.check(&ctx).unwrap();
751
752 assert_eq!(result.len(), 2);
754 assert!(result[0].message.contains("Missing 2 blank lines before table"));
755 assert!(result[1].message.contains("Missing 2 blank lines after table"));
756 }
757
758 #[test]
759 fn test_config_zero_minimum() {
760 let config = MD058Config {
761 minimum_before: 0,
762 minimum_after: 0,
763 };
764 let rule = MD058BlanksAroundTables::from_config_struct(config);
765
766 let content = "Text before.
767| Header | Col 2 |
768|--------|-------|
769| Cell | Data |
770More text.";
771 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
772 let result = rule.check(&ctx).unwrap();
773
774 assert_eq!(result.len(), 0);
776 }
777
778 #[test]
779 fn test_fix_with_custom_config() {
780 let config = MD058Config {
781 minimum_before: 2,
782 minimum_after: 3,
783 };
784 let rule = MD058BlanksAroundTables::from_config_struct(config);
785
786 let content = "Text before.
787| Header | Col 2 |
788|--------|-------|
789| Cell | Data |
790Text after.";
791 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
792 let fixed = rule.fix(&ctx).unwrap();
793
794 let expected = "Text before.
795
796
797| Header | Col 2 |
798|--------|-------|
799| Cell | Data |
800
801
802
803Text after.";
804 assert_eq!(fixed, expected);
805 }
806
807 #[test]
808 fn test_default_config_section() {
809 let rule = MD058BlanksAroundTables::default();
810 let config_section = rule.default_config_section();
811
812 assert!(config_section.is_some());
813 let (name, value) = config_section.unwrap();
814 assert_eq!(name, "MD058");
815
816 if let toml::Value::Table(table) = value {
818 assert!(table.contains_key("minimum-before"));
819 assert!(table.contains_key("minimum-after"));
820 assert_eq!(table["minimum-before"], toml::Value::Integer(1));
821 assert_eq!(table["minimum-after"], toml::Value::Integer(1));
822 } else {
823 panic!("Expected TOML table");
824 }
825 }
826
827 #[test]
828 fn test_blank_lines_counting() {
829 let rule = MD058BlanksAroundTables::default();
830 let lines = vec!["text", "", "", "table", "more", "", "end"];
831
832 assert_eq!(rule.count_blank_lines_before(&lines, 3), 2);
834
835 assert_eq!(rule.count_blank_lines_after(&lines, 4), 1);
837
838 assert_eq!(rule.count_blank_lines_before(&lines, 0), 0);
840
841 assert_eq!(rule.count_blank_lines_after(&lines, 6), 0);
843 }
844
845 #[test]
846 fn test_issue_25_table_with_long_line() {
847 let rule = MD058BlanksAroundTables::default();
849 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 |";
850 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
851
852 let table_blocks = TableUtils::find_table_blocks(content, &ctx);
854 for (i, block) in table_blocks.iter().enumerate() {
855 eprintln!(
856 "Table {}: start={}, end={}, header={}, delimiter={}, content_lines={:?}",
857 i + 1,
858 block.start_line + 1,
859 block.end_line + 1,
860 block.header_line + 1,
861 block.delimiter_line + 1,
862 block.content_lines.iter().map(|x| x + 1).collect::<Vec<_>>()
863 );
864 }
865
866 let result = rule.check(&ctx).unwrap();
867
868 assert_eq!(table_blocks.len(), 1, "Should detect exactly one table block");
870
871 assert_eq!(result.len(), 0, "Should not flag any MD058 issues for a complete table");
873 }
874
875 #[test]
876 fn test_fix_preserves_blockquote_prefix_before_table() {
877 let rule = MD058BlanksAroundTables::default();
879
880 let content = "> Text before
881> | H1 | H2 |
882> |----|---|
883> | a | b |";
884 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
885 let fixed = rule.fix(&ctx).unwrap();
886
887 let expected = "> Text before
889>
890> | H1 | H2 |
891> |----|---|
892> | a | b |";
893 assert_eq!(
894 fixed, expected,
895 "Fix should insert '>' blank line before table, not plain blank line"
896 );
897 }
898
899 #[test]
900 fn test_fix_preserves_blockquote_prefix_after_table() {
901 let rule = MD058BlanksAroundTables::default();
903
904 let content = "> | H1 | H2 |
905> |----|---|
906> | a | b |
907> Text after";
908 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
909 let fixed = rule.fix(&ctx).unwrap();
910
911 let expected = "> | H1 | H2 |
913> |----|---|
914> | a | b |
915>
916> Text after";
917 assert_eq!(
918 fixed, expected,
919 "Fix should insert '>' blank line after table, not plain blank line"
920 );
921 }
922
923 #[test]
924 fn test_fix_preserves_nested_blockquote_prefix_for_table() {
925 let rule = MD058BlanksAroundTables::default();
927
928 let content = ">> Nested quote
929>> | H1 |
930>> |----|
931>> | a |
932>> More text";
933 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
934 let fixed = rule.fix(&ctx).unwrap();
935
936 let expected = ">> Nested quote
938>>
939>> | H1 |
940>> |----|
941>> | a |
942>>
943>> More text";
944 assert_eq!(fixed, expected, "Fix should preserve nested blockquote prefix '>>'");
945 }
946
947 #[test]
948 fn test_fix_preserves_triple_nested_blockquote_prefix_for_table() {
949 let rule = MD058BlanksAroundTables::default();
951
952 let content = ">>> Triple nested
953>>> | A | B |
954>>> |---|---|
955>>> | 1 | 2 |
956>>> More text";
957 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
958 let fixed = rule.fix(&ctx).unwrap();
959
960 let expected = ">>> Triple nested
961>>>
962>>> | A | B |
963>>> |---|---|
964>>> | 1 | 2 |
965>>>
966>>> More text";
967 assert_eq!(
968 fixed, expected,
969 "Fix should preserve triple-nested blockquote prefix '>>>'"
970 );
971 }
972
973 #[test]
980 fn test_is_blank_line_with_blockquote_continuation() {
981 let rule = MD058BlanksAroundTables::default();
983
984 assert!(rule.is_blank_line(""));
986 assert!(rule.is_blank_line(" "));
987 assert!(rule.is_blank_line("\t"));
988 assert!(rule.is_blank_line(" \t "));
989
990 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("> > "));
998 assert!(rule.is_blank_line("> > > "));
999 assert!(rule.is_blank_line(" > ")); assert!(!rule.is_blank_line("text"));
1003 assert!(!rule.is_blank_line("> text"));
1004 assert!(!rule.is_blank_line(">> text"));
1005 assert!(!rule.is_blank_line("> | table |"));
1006 assert!(!rule.is_blank_line("| table |"));
1007 }
1008
1009 #[test]
1010 fn test_issue_305_no_warning_blockquote_with_existing_blank_before_table() {
1011 let rule = MD058BlanksAroundTables::default();
1014
1015 let content = "> Text before
1016>
1017> | H1 | H2 |
1018> |----|---|
1019> | a | b |";
1020 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1021 let result = rule.check(&ctx).unwrap();
1022
1023 assert_eq!(
1024 result.len(),
1025 0,
1026 "Should not warn when blockquote already has blank line before table"
1027 );
1028 }
1029
1030 #[test]
1031 fn test_issue_305_no_warning_blockquote_with_existing_blank_after_table() {
1032 let rule = MD058BlanksAroundTables::default();
1035
1036 let content = "> | H1 | H2 |
1037> |----|---|
1038> | a | b |
1039>
1040> Text after";
1041 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1042 let result = rule.check(&ctx).unwrap();
1043
1044 assert_eq!(
1045 result.len(),
1046 0,
1047 "Should not warn when blockquote already has blank line after table"
1048 );
1049 }
1050
1051 #[test]
1052 fn test_issue_305_no_warning_blockquote_with_both_blank_lines() {
1053 let rule = MD058BlanksAroundTables::default();
1055
1056 let content = "> The following options are available:
1057>
1058> | Option | Default | Description |
1059> |--------|-----------|-------------------|
1060> | port | 3000 | Server port |
1061> | host | localhost | Server host |";
1062 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1063 let result = rule.check(&ctx).unwrap();
1064
1065 assert_eq!(
1066 result.len(),
1067 0,
1068 "Issue #305: Should not warn for valid table inside blockquote with blank line"
1069 );
1070 }
1071
1072 #[test]
1073 fn test_issue_305_no_warning_nested_blockquote_with_blank_lines() {
1074 let rule = MD058BlanksAroundTables::default();
1076
1077 let content = ">> Nested text
1078>>
1079>> | Col1 | Col2 |
1080>> |------|------|
1081>> | val1 | val2 |
1082>>
1083>> More text";
1084 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1085 let result = rule.check(&ctx).unwrap();
1086
1087 assert_eq!(
1088 result.len(),
1089 0,
1090 "Should not warn for nested blockquote table with blank lines"
1091 );
1092 }
1093
1094 #[test]
1095 fn test_issue_305_no_warning_triple_nested_blockquote_with_blank_lines() {
1096 let rule = MD058BlanksAroundTables::default();
1098
1099 let content = ">>> Deep nesting
1100>>>
1101>>> | A | B |
1102>>> |---|---|
1103>>> | 1 | 2 |
1104>>>
1105>>> End";
1106 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1107 let result = rule.check(&ctx).unwrap();
1108
1109 assert_eq!(
1110 result.len(),
1111 0,
1112 "Should not warn for triple-nested blockquote table with blank lines"
1113 );
1114 }
1115
1116 #[test]
1117 fn test_issue_305_fix_does_not_corrupt_valid_blockquote_table() {
1118 let rule = MD058BlanksAroundTables::default();
1120
1121 let content = "> Text before
1122>
1123> | H1 | H2 |
1124> |----|---|
1125> | a | b |
1126>
1127> Text after";
1128 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1129 let fixed = rule.fix(&ctx).unwrap();
1130
1131 assert_eq!(fixed, content, "Fix should not modify already-valid blockquote table");
1132 }
1133
1134 #[test]
1135 fn test_issue_305_blockquote_blank_with_trailing_space() {
1136 let rule = MD058BlanksAroundTables::default();
1138
1139 let content = "> Text before
1141>
1142> | H1 | H2 |
1143> |----|---|
1144> | a | b |";
1145 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1146 let result = rule.check(&ctx).unwrap();
1147
1148 assert_eq!(
1149 result.len(),
1150 0,
1151 "Should recognize '> ' (with trailing space) as blank line"
1152 );
1153 }
1154
1155 #[test]
1156 fn test_issue_305_spaced_nested_blockquote() {
1157 let rule = MD058BlanksAroundTables::default();
1159
1160 let content = "> > Nested text
1161> >
1162> > | H1 |
1163> > |----|
1164> > | a |";
1165 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1166 let result = rule.check(&ctx).unwrap();
1167
1168 assert_eq!(
1169 result.len(),
1170 0,
1171 "Should recognize '> > ' style nested blockquote blank line"
1172 );
1173 }
1174
1175 #[test]
1176 fn test_mixed_regular_and_blockquote_tables() {
1177 let rule = MD058BlanksAroundTables::default();
1179
1180 let content = "# Mixed Content
1181
1182Regular table:
1183
1184| A | B |
1185|---|---|
1186| 1 | 2 |
1187
1188And a blockquote table:
1189
1190> Quote text
1191>
1192> | X | Y |
1193> |---|---|
1194> | 3 | 4 |
1195>
1196> End quote
1197
1198Final paragraph.";
1199 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1200 let result = rule.check(&ctx).unwrap();
1201
1202 assert_eq!(
1203 result.len(),
1204 0,
1205 "Should handle mixed regular and blockquote tables correctly"
1206 );
1207 }
1208
1209 #[test]
1210 fn test_blockquote_table_at_document_start() {
1211 let rule = MD058BlanksAroundTables::default();
1213
1214 let content = "> | H1 | H2 |
1215> |----|---|
1216> | a | b |
1217>
1218> Text after";
1219 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1220 let result = rule.check(&ctx).unwrap();
1221
1222 assert_eq!(
1223 result.len(),
1224 0,
1225 "Should not require blank line before table at document start (even in blockquote)"
1226 );
1227 }
1228
1229 #[test]
1230 fn test_blockquote_table_at_document_end() {
1231 let rule = MD058BlanksAroundTables::default();
1233
1234 let content = "> Text before
1235>
1236> | H1 | H2 |
1237> |----|---|
1238> | a | b |";
1239 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1240 let result = rule.check(&ctx).unwrap();
1241
1242 assert_eq!(
1243 result.len(),
1244 0,
1245 "Should not require blank line after table at document end"
1246 );
1247 }
1248
1249 #[test]
1250 fn test_blockquote_table_missing_blank_still_detected() {
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 result = rule.check(&ctx).unwrap();
1261
1262 assert_eq!(
1264 result.len(),
1265 2,
1266 "Should still detect missing blank lines in blockquote tables"
1267 );
1268 assert!(result[0].message.contains("before table"));
1269 assert!(result[1].message.contains("after table"));
1270 }
1271
1272 #[test]
1273 fn test_blockquote_table_fix_adds_correct_prefix() {
1274 let rule = MD058BlanksAroundTables::default();
1276
1277 let content = "> Text before
1278> | H1 | H2 |
1279> |----|---|
1280> | a | b |
1281> Text after";
1282 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1283 let fixed = rule.fix(&ctx).unwrap();
1284
1285 let expected = "> Text before
1286>
1287> | H1 | H2 |
1288> |----|---|
1289> | a | b |
1290>
1291> Text after";
1292 assert_eq!(fixed, expected, "Fix should add blockquote-prefixed blank lines");
1293 }
1294
1295 #[test]
1296 fn test_multiple_blockquote_tables_with_valid_spacing() {
1297 let rule = MD058BlanksAroundTables::default();
1299
1300 let content = "> First table:
1301>
1302> | A | B |
1303> |---|---|
1304> | 1 | 2 |
1305>
1306> Second table:
1307>
1308> | X | Y |
1309> |---|---|
1310> | 3 | 4 |
1311>
1312> End";
1313 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1314 let result = rule.check(&ctx).unwrap();
1315
1316 assert_eq!(
1317 result.len(),
1318 0,
1319 "Should handle multiple blockquote tables with valid spacing"
1320 );
1321 }
1322
1323 #[test]
1324 fn test_blockquote_table_with_minimum_before_config() {
1325 let config = MD058Config {
1327 minimum_before: 2,
1328 minimum_after: 1,
1329 };
1330 let rule = MD058BlanksAroundTables::from_config_struct(config);
1331
1332 let content = "> Text
1333>
1334> | H1 |
1335> |----|
1336> | a |";
1337 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1338 let result = rule.check(&ctx).unwrap();
1339
1340 assert_eq!(result.len(), 1);
1342 assert!(result[0].message.contains("before table"));
1343 }
1344}