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