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