1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::rule_config_serde::RuleConfig;
3use crate::utils::mkdocs_attr_list::is_block_attribute_line;
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 {
65 crate::utils::regex_cache::is_blank_in_blockquote_context(line)
66 || crate::utils::blank_lines::is_blank_or_comment_only(line)
67 }
68
69 fn count_blank_lines_before(&self, lines: &[&str], line_index: usize) -> usize {
71 let mut count = 0;
72 let mut i = line_index;
73 while i > 0 {
74 i -= 1;
75 if self.is_blank_line(lines[i]) {
76 count += 1;
77 } else {
78 break;
79 }
80 }
81 count
82 }
83
84 fn count_blank_lines_after(&self, lines: &[&str], line_index: usize) -> usize {
86 let mut count = 0;
87 let mut i = line_index + 1;
88 while i < lines.len() {
89 if self.is_blank_line(lines[i]) {
90 count += 1;
91 i += 1;
92 } else {
93 break;
94 }
95 }
96 count
97 }
98}
99
100fn is_thematic_break(text: &str) -> bool {
102 let mut chars = text.chars().filter(|c| !matches!(c, ' ' | '\t'));
103 let Some(first) = chars.next().filter(|c| matches!(c, '-' | '*' | '_')) else {
104 return false;
105 };
106 let mut count = 1;
107 for c in chars {
108 if c != first {
109 return false;
110 }
111 count += 1;
112 }
113 count >= 3
114}
115
116fn is_bare_list_marker_line(line: &str) -> bool {
119 let mut rest = line;
120 let mut last_is_list = false;
121 let mut markers = 0usize;
122 loop {
123 rest = rest.trim_start_matches([' ', '\t']);
124 let Some(first) = rest.chars().next() else {
125 break;
126 };
127 if is_thematic_break(rest) {
128 return false;
129 }
130 let after_marker = |len: usize| rest[len..].chars().next().is_none_or(|c| c == ' ' || c == '\t');
131 if first == '>' {
132 rest = &rest[1..];
133 last_is_list = false;
134 } else if matches!(first, '-' | '*' | '+') && after_marker(1) {
135 rest = &rest[1..];
136 last_is_list = true;
137 } else {
138 let digits = rest.chars().take_while(char::is_ascii_digit).count();
139 let terminator = rest[digits..].chars().next();
140 if (1..=9).contains(&digits) && matches!(terminator, Some('.' | ')')) && after_marker(digits + 1) {
141 rest = &rest[digits + 1..];
142 last_is_list = true;
143 } else {
144 return false;
145 }
146 }
147 markers += 1;
148 }
149 last_is_list && markers > 0
150}
151
152impl Rule for MD058BlanksAroundTables {
153 fn native_format_policy(&self) -> Option<crate::format_proof::NativeFormatPolicy> {
154 (self.config == Default::default()).then_some(crate::format_proof::NativeFormatPolicy::for_rule::<Self>(58))
155 }
156
157 fn name(&self) -> &'static str {
158 "MD058"
159 }
160
161 fn description(&self) -> &'static str {
162 "Tables should be surrounded by blank lines"
163 }
164
165 fn category(&self) -> RuleCategory {
166 RuleCategory::Table
167 }
168
169 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
170 !ctx.likely_has_tables()
172 }
173
174 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
175 let content = ctx.content;
176 let mut warnings = Vec::new();
177
178 if content.is_empty() || !content.contains('|') {
180 return Ok(Vec::new());
181 }
182
183 let lines = ctx.raw_lines();
184
185 let table_blocks = &ctx.table_blocks;
187
188 for table_block in table_blocks.iter().filter(|block| block.parser_confirmed) {
189 let item_content = table_block.list_context.is_some()
195 && table_block.start_line > 0
196 && is_bare_list_marker_line(lines[table_block.start_line - 1])
197 && ctx.list_item_on_line(table_block.start_line).is_some();
198 let opens_container = table_block.start_line > 0
202 && ctx.flavor != crate::config::MarkdownFlavor::MDX
203 && ctx.lines[table_block.start_line - 1].in_mkdocs_html_markdown
204 && crate::utils::mkdocs_html_markdown::opens_markdown_block_alone(lines[table_block.start_line - 1]);
205 let admonition_first_line = ctx.flavor == crate::config::MarkdownFlavor::MkDocs
208 && table_block.start_line > 0
209 && crate::utils::mkdocs_admonitions::is_admonition_start(lines[table_block.start_line - 1])
210 && crate::utils::mkdocs_admonitions::is_admonition_content(
211 lines[table_block.start_line],
212 lines[table_block.start_line - 1].len() - lines[table_block.start_line - 1].trim_start().len(),
213 );
214 if table_block.start_line > 0 && !item_content && !opens_container && !admonition_first_line {
215 let blank_lines_before = self.count_blank_lines_before(lines, table_block.start_line);
216 if blank_lines_before < self.config.minimum_before {
217 let needed = self.config.minimum_before - blank_lines_before;
218 let message = if self.config.minimum_before == 1 {
219 "Missing blank line before table".to_string()
220 } else {
221 format!("Missing {needed} blank lines before table")
222 };
223
224 let bq_prefix = ctx.blockquote_prefix_for_blank_line(table_block.start_line);
225 let replacement = format!("{bq_prefix}\n").repeat(needed);
226 let fix = (ctx.flavor == crate::config::MarkdownFlavor::MDG
227 || !crate::utils::table_utils::TableUtils::header_is_indented_as_code(
228 lines[table_block.start_line],
229 lines[table_block.delimiter_line],
230 table_block,
231 ))
232 .then(|| Fix::new(ctx.line_column_byte_range(table_block.start_line + 1, 1), replacement));
233 warnings.push(LintWarning {
234 rule_name: Some(self.name().to_string()),
235 message,
236 line: table_block.start_line + 1,
237 column: 1,
238 end_line: table_block.start_line + 1,
239 end_column: 2,
240 severity: Severity::Warning,
241 fix,
242 });
243 }
244 }
245
246 if table_block.end_line < lines.len() - 1 {
248 let next_line_is_attribute = if table_block.end_line + 1 < lines.len() {
250 is_block_attribute_line(lines[table_block.end_line + 1], ctx.flavor)
251 || (ctx.flavor != crate::config::MarkdownFlavor::MDX
253 && ctx.lines[table_block.end_line + 1].closes_markdown_element)
254 } else {
255 false
256 };
257
258 if !next_line_is_attribute {
260 let blank_lines_after = self.count_blank_lines_after(lines, table_block.end_line);
261 if blank_lines_after < self.config.minimum_after {
262 let needed = self.config.minimum_after - blank_lines_after;
263 let message = if self.config.minimum_after == 1 {
264 "Missing blank line after table".to_string()
265 } else {
266 format!("Missing {needed} blank lines after table")
267 };
268
269 let bq_prefix = ctx.blockquote_prefix_for_blank_line(table_block.end_line);
270 let replacement = format!("{bq_prefix}\n").repeat(needed);
271 warnings.push(LintWarning {
272 rule_name: Some(self.name().to_string()),
273 message,
274 line: table_block.end_line + 1,
275 column: lines[table_block.end_line].chars().count() + 1,
276 end_line: table_block.end_line + 1,
277 end_column: lines[table_block.end_line].chars().count() + 2,
278 severity: Severity::Warning,
279 fix: Some(Fix::new(
280 ctx.line_column_byte_range(
281 table_block.end_line + 1,
282 lines[table_block.end_line].len() + 1,
283 ),
284 replacement,
285 )),
286 });
287 }
288 }
289 }
290 }
291
292 Ok(warnings)
293 }
294
295 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
296 let content = ctx.content;
297
298 let warnings = self.check(ctx)?;
299 let mut warnings =
300 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
301 warnings.retain(|warning| warning.fix.is_some());
302 if warnings.is_empty() {
303 return Ok(content.to_string());
304 }
305
306 let lines = ctx.raw_lines();
307 let mut result = Vec::new();
308 let mut i = 0;
309
310 while i < lines.len() {
311 let warning_before = warnings
313 .iter()
314 .position(|w| w.line == i + 1 && w.message.contains("before table"));
315
316 if let Some(idx) = warning_before {
317 let warning = &warnings[idx];
318 let needed_blanks = if warning.message.contains("Missing blank line before") {
320 1
321 } else if let Some(start) = warning.message.find("Missing ") {
322 if let Some(end) = warning.message.find(" blank lines before") {
323 warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
324 } else {
325 1
326 }
327 } else {
328 1
329 };
330
331 let bq_prefix = ctx.blockquote_prefix_for_blank_line(i);
333 for _ in 0..needed_blanks {
334 result.push(bq_prefix.clone());
335 }
336 warnings.remove(idx);
337 }
338
339 result.push(lines[i].to_string());
340
341 let warning_after = warnings
343 .iter()
344 .position(|w| w.line == i + 1 && w.message.contains("after table"));
345
346 if let Some(idx) = warning_after {
347 let warning = &warnings[idx];
348 let needed_blanks = if warning.message.contains("Missing blank line after") {
350 1
351 } else if let Some(start) = warning.message.find("Missing ") {
352 if let Some(end) = warning.message.find(" blank lines after") {
353 warning.message[start + 8..end].parse::<usize>().unwrap_or(1)
354 } else {
355 1
356 }
357 } else {
358 1
359 };
360
361 let bq_prefix = ctx.blockquote_prefix_for_blank_line(i);
363 for _ in 0..needed_blanks {
364 result.push(bq_prefix.clone());
365 }
366 warnings.remove(idx);
367 }
368
369 i += 1;
370 }
371
372 let mut fixed = result.join("\n");
373 if content.ends_with('\n') {
374 fixed.push('\n');
375 }
376
377 Ok(fixed)
378 }
379
380 fn as_any(&self) -> &dyn std::any::Any {
381 self
382 }
383
384 crate::impl_rule_config_methods!(MD058Config);
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use crate::lint_context::LintContext;
391 use crate::utils::table_utils::TableUtils;
392
393 #[test]
394 fn test_table_with_blanks() {
395 let rule = MD058BlanksAroundTables::default();
396 let content = "Some text before.
397
398| Header 1 | Header 2 |
399|----------|----------|
400| Cell 1 | Cell 2 |
401
402Some text after.";
403 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
404 let result = rule.check(&ctx).unwrap();
405
406 assert_eq!(result.len(), 0);
407 }
408
409 #[test]
410 fn test_table_missing_blank_before() {
411 let rule = MD058BlanksAroundTables::default();
412 let content = "Some text before.
413| Header 1 | Header 2 |
414|----------|----------|
415| Cell 1 | Cell 2 |
416
417Some text after.";
418 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
419 let result = rule.check(&ctx).unwrap();
420
421 assert_eq!(result.len(), 1);
422 assert_eq!(result[0].line, 2);
423 assert!(result[0].message.contains("Missing blank line before table"));
424 }
425
426 #[test]
427 fn test_table_missing_blank_after() {
428 let rule = MD058BlanksAroundTables::default();
429 let content = "Some text before.
430
431| Header 1 | Header 2 |
432|----------|----------|
433| Cell 1 | Cell 2 |
434## Some text after";
435 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
436 let result = rule.check(&ctx).unwrap();
437
438 assert_eq!(result.len(), 1);
439 assert_eq!(result[0].line, 5);
440 assert!(result[0].message.contains("Missing blank line after table"));
441 }
442
443 #[test]
444 fn test_table_missing_both_blanks() {
445 let rule = MD058BlanksAroundTables::default();
446 let content = "Some text before.
447| Header 1 | Header 2 |
448|----------|----------|
449| Cell 1 | Cell 2 |
450## Some text after";
451 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
452 let result = rule.check(&ctx).unwrap();
453
454 assert_eq!(result.len(), 2);
455 assert!(result[0].message.contains("Missing blank line before table"));
456 assert!(result[1].message.contains("Missing blank line after table"));
457 }
458
459 #[test]
460 fn test_table_at_start_of_document() {
461 let rule = MD058BlanksAroundTables::default();
462 let content = "| Header 1 | Header 2 |
463|----------|----------|
464| Cell 1 | Cell 2 |
465
466Some text after.";
467 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
468 let result = rule.check(&ctx).unwrap();
469
470 assert_eq!(result.len(), 0);
472 }
473
474 #[test]
475 fn test_table_at_end_of_document() {
476 let rule = MD058BlanksAroundTables::default();
477 let content = "Some text before.
478
479| Header 1 | Header 2 |
480|----------|----------|
481| Cell 1 | Cell 2 |";
482 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
483 let result = rule.check(&ctx).unwrap();
484
485 assert_eq!(result.len(), 0);
487 }
488
489 #[test]
490 fn test_multiple_tables() {
491 let rule = MD058BlanksAroundTables::default();
492 let content = "Text before first table.
493| Col 1 | Col 2 |
494|--------|-------|
495| Data 1 | Val 1 |
496## Between tables
497| Col A | Col B |
498|--------|-------|
499| Data 2 | Val 2 |
500## After the tables";
501 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
502 let result = rule.check(&ctx).unwrap();
503
504 assert_eq!(result.len(), 4);
505 assert!(result[0].message.contains("Missing blank line before table"));
507 assert!(result[1].message.contains("Missing blank line after table"));
508 assert!(result[2].message.contains("Missing blank line before table"));
510 assert!(result[3].message.contains("Missing blank line after table"));
511 }
512
513 #[test]
514 fn test_consecutive_tables() {
515 let rule = MD058BlanksAroundTables::default();
516 let content = "Some text.
517
518| Col 1 | Col 2 |
519|--------|-------|
520| Data 1 | Val 1 |
521
522| Col A | Col B |
523|--------|-------|
524| Data 2 | Val 2 |
525
526More text.";
527 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
528 let result = rule.check(&ctx).unwrap();
529
530 assert_eq!(result.len(), 0);
532 }
533
534 #[test]
535 fn test_consecutive_tables_no_blank() {
536 let rule = MD058BlanksAroundTables::default();
537 let content = "Some text.
539
540| Col 1 | Col 2 |
541|--------|-------|
542| Data 1 | Val 1 |
543## Between
544| Col A | Col B |
545|--------|-------|
546| Data 2 | Val 2 |
547
548More text.";
549 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
550 let result = rule.check(&ctx).unwrap();
551
552 assert_eq!(result.len(), 2);
554 assert!(result[0].message.contains("Missing blank line after table"));
555 assert!(result[1].message.contains("Missing blank line before table"));
556 }
557
558 #[test]
559 fn test_fix_missing_blanks() {
560 let rule = MD058BlanksAroundTables::default();
561 let content = "Text before.
562| Header | Col 2 |
563|--------|-------|
564| Cell | Data |
565# Text after";
566 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
567 let fixed = rule.fix(&ctx).unwrap();
568
569 let expected = "Text before.
570
571| Header | Col 2 |
572|--------|-------|
573| Cell | Data |
574
575# Text after";
576 assert_eq!(fixed, expected);
577 }
578
579 #[test]
580 fn test_fix_multiple_tables() {
581 let rule = MD058BlanksAroundTables::default();
582 let content = "Start
583| T1 | C1 |
584|----|----|
585| D1 | V1 |
586# Middle
587| T2 | C2 |
588|----|----|
589| D2 | V2 |
590# End";
591 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
592 let fixed = rule.fix(&ctx).unwrap();
593
594 let expected = "Start
595
596| T1 | C1 |
597|----|----|
598| D1 | V1 |
599
600# Middle
601
602| T2 | C2 |
603|----|----|
604| D2 | V2 |
605
606# End";
607 assert_eq!(fixed, expected);
608 }
609
610 #[test]
611 fn test_empty_content() {
612 let rule = MD058BlanksAroundTables::default();
613 let content = "";
614 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
615 let result = rule.check(&ctx).unwrap();
616
617 assert_eq!(result.len(), 0);
618 }
619
620 #[test]
621 fn test_no_tables() {
622 let rule = MD058BlanksAroundTables::default();
623 let content = "Just regular text.
624No tables here.
625Only paragraphs.";
626 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
627 let result = rule.check(&ctx).unwrap();
628
629 assert_eq!(result.len(), 0);
630 }
631
632 #[test]
633 fn test_code_block_with_table() {
634 let rule = MD058BlanksAroundTables::default();
635 let content = "Text before.
636```
637| Not | A | Table |
638|-----|---|-------|
639| In | Code | Block |
640```
641Text after.";
642 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
643 let result = rule.check(&ctx).unwrap();
644
645 assert_eq!(result.len(), 0);
647 }
648
649 #[test]
650 fn test_table_with_complex_content() {
651 let rule = MD058BlanksAroundTables::default();
652 let content = "# Heading
653| Column 1 | Column 2 | Column 3 |
654|:---------|:--------:|---------:|
655| Left | Center | Right |
656| Data | More | Info |
657## Another Heading";
658 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
659 let result = rule.check(&ctx).unwrap();
660
661 assert_eq!(result.len(), 2);
662 assert!(result[0].message.contains("Missing blank line before table"));
663 assert!(result[1].message.contains("Missing blank line after table"));
664 }
665
666 #[test]
667 fn test_table_with_empty_cells() {
668 let rule = MD058BlanksAroundTables::default();
669 let content = "Text.
670
671| | | |
672|-----|-----|-----|
673| | X | |
674| O | | X |
675
676More text.";
677 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
678 let result = rule.check(&ctx).unwrap();
679
680 assert_eq!(result.len(), 0);
681 }
682
683 #[test]
684 fn test_table_with_unicode() {
685 let rule = MD058BlanksAroundTables::default();
686 let content = "Unicode test.
687| 名前 | 年齢 | 都市 |
688|------|------|------|
689| 田中 | 25 | 東京 |
690| 佐藤 | 30 | 大阪 |
691## End";
692 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
693 let result = rule.check(&ctx).unwrap();
694
695 assert_eq!(result.len(), 2);
696 }
697
698 #[test]
699 fn test_table_with_long_cells() {
700 let rule = MD058BlanksAroundTables::default();
701 let content = "Before.
702
703| Short | Very very very very very very very very long header |
704|-------|-----------------------------------------------------|
705| Data | This is an extremely long cell content that goes on |
706
707After.";
708 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
709 let result = rule.check(&ctx).unwrap();
710
711 assert_eq!(result.len(), 0);
712 }
713
714 #[test]
715 fn test_table_without_content_rows() {
716 let rule = MD058BlanksAroundTables::default();
717 let content = "Text.
718| Header 1 | Header 2 |
719|----------|----------|
720## Next section";
721 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
722 let result = rule.check(&ctx).unwrap();
723
724 assert_eq!(result.len(), 2);
726 }
727
728 #[test]
729 fn test_indented_table() {
730 let rule = MD058BlanksAroundTables::default();
731 let content = "List item:
732
733 | Indented | Table |
734 |----------|-------|
735 | Data | Here |
736
737 More content.";
738 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
739 let result = rule.check(&ctx).unwrap();
740
741 assert_eq!(result.len(), 0);
743 }
744
745 #[test]
746 fn test_single_column_table_not_detected() {
747 let rule = MD058BlanksAroundTables::default();
748 let content = "Text before.
749| Single |
750|--------|
751| Column |
752## Text after";
753 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
754 let result = rule.check(&ctx).unwrap();
755
756 assert_eq!(result.len(), 2);
759 assert!(result[0].message.contains("before"));
760 assert!(result[1].message.contains("after"));
761 }
762
763 #[test]
764 fn test_config_minimum_before() {
765 let config = MD058Config {
766 minimum_before: 2,
767 minimum_after: 1,
768 };
769 let rule = MD058BlanksAroundTables::from_config_struct(config);
770
771 let content = "Text before.
772
773| Header | Col 2 |
774|--------|-------|
775| Cell | Data |
776
777Text after.";
778 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
779 let result = rule.check(&ctx).unwrap();
780
781 assert_eq!(result.len(), 1);
783 assert!(result[0].message.contains("Missing 1 blank lines before table"));
784 }
785
786 #[test]
787 fn test_config_minimum_after() {
788 let config = MD058Config {
789 minimum_before: 1,
790 minimum_after: 3,
791 };
792 let rule = MD058BlanksAroundTables::from_config_struct(config);
793
794 let content = "Text before.
795
796| Header | Col 2 |
797|--------|-------|
798| Cell | Data |
799
800More text.";
801 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
802 let result = rule.check(&ctx).unwrap();
803
804 assert_eq!(result.len(), 1);
806 assert!(result[0].message.contains("Missing 2 blank lines after table"));
807 }
808
809 #[test]
810 fn test_config_both_minimum() {
811 let config = MD058Config {
812 minimum_before: 2,
813 minimum_after: 2,
814 };
815 let rule = MD058BlanksAroundTables::from_config_struct(config);
816
817 let content = "Text before.
818| Header | Col 2 |
819|--------|-------|
820| Cell | Data |
821## More text";
822 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
823 let result = rule.check(&ctx).unwrap();
824
825 assert_eq!(result.len(), 2);
827 assert!(result[0].message.contains("Missing 2 blank lines before table"));
828 assert!(result[1].message.contains("Missing 2 blank lines after table"));
829 }
830
831 #[test]
832 fn test_config_zero_minimum() {
833 let config = MD058Config {
834 minimum_before: 0,
835 minimum_after: 0,
836 };
837 let rule = MD058BlanksAroundTables::from_config_struct(config);
838
839 let content = "Text before.
840| Header | Col 2 |
841|--------|-------|
842| Cell | Data |
843More text.";
844 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
845 let result = rule.check(&ctx).unwrap();
846
847 assert_eq!(result.len(), 0);
849 }
850
851 #[test]
852 fn test_fix_with_custom_config() {
853 let config = MD058Config {
854 minimum_before: 2,
855 minimum_after: 3,
856 };
857 let rule = MD058BlanksAroundTables::from_config_struct(config);
858
859 let content = "Text before.
860| Header | Col 2 |
861|--------|-------|
862| Cell | Data |
863# Text after";
864 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
865 let fixed = rule.fix(&ctx).unwrap();
866
867 let expected = "Text before.
868
869
870| Header | Col 2 |
871|--------|-------|
872| Cell | Data |
873
874
875
876# Text after";
877 assert_eq!(fixed, expected);
878 }
879
880 #[test]
881 fn test_default_config_section() {
882 let rule = MD058BlanksAroundTables::default();
883 let config_section = rule.default_config_section();
884
885 assert!(config_section.is_some());
886 let (name, value) = config_section.unwrap();
887 assert_eq!(name, "MD058");
888
889 if let toml::Value::Table(table) = value {
891 assert!(table.contains_key("minimum-before"));
892 assert!(table.contains_key("minimum-after"));
893 assert_eq!(table["minimum-before"], toml::Value::Integer(1));
894 assert_eq!(table["minimum-after"], toml::Value::Integer(1));
895 } else {
896 panic!("Expected TOML table");
897 }
898 }
899
900 #[test]
901 fn test_blank_lines_counting() {
902 let rule = MD058BlanksAroundTables::default();
903 let lines = vec!["text", "", "", "table", "more", "", "end"];
904
905 assert_eq!(rule.count_blank_lines_before(&lines, 3), 2);
907
908 assert_eq!(rule.count_blank_lines_after(&lines, 4), 1);
910
911 assert_eq!(rule.count_blank_lines_before(&lines, 0), 0);
913
914 assert_eq!(rule.count_blank_lines_after(&lines, 6), 0);
916 }
917
918 #[test]
919 fn test_issue_25_table_with_long_line() {
920 let rule = MD058BlanksAroundTables::default();
922 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 |";
923 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
924
925 let table_blocks = TableUtils::find_table_blocks(content, &ctx);
927 for (i, block) in table_blocks.iter().enumerate() {
928 eprintln!(
929 "Table {}: start={}, end={}, header={}, delimiter={}, content_lines={:?}",
930 i + 1,
931 block.start_line + 1,
932 block.end_line + 1,
933 block.header_line + 1,
934 block.delimiter_line + 1,
935 block.content_lines.iter().map(|x| x + 1).collect::<Vec<_>>()
936 );
937 }
938
939 let result = rule.check(&ctx).unwrap();
940
941 assert_eq!(table_blocks.len(), 1, "Should detect exactly one table block");
943
944 assert_eq!(result.len(), 0, "Should not flag any MD058 issues for a complete table");
946 }
947
948 #[test]
949 fn test_fix_preserves_blockquote_prefix_before_table() {
950 let rule = MD058BlanksAroundTables::default();
952
953 let content = "> Text before
954> | H1 | H2 |
955> |----|---|
956> | a | b |";
957 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
958 let fixed = rule.fix(&ctx).unwrap();
959
960 let expected = "> Text before
962>
963> | H1 | H2 |
964> |----|---|
965> | a | b |";
966 assert_eq!(
967 fixed, expected,
968 "Fix should insert '>' blank line before table, not plain blank line"
969 );
970 }
971
972 #[test]
973 fn test_fix_preserves_blockquote_prefix_after_table() {
974 let rule = MD058BlanksAroundTables::default();
976
977 let content = "> | H1 | H2 |
978> |----|---|
979> | a | b |
980> # Text after";
981 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
982 let fixed = rule.fix(&ctx).unwrap();
983
984 let expected = "> | H1 | H2 |
986> |----|---|
987> | a | b |
988>
989> # Text after";
990 assert_eq!(
991 fixed, expected,
992 "Fix should insert '>' blank line after table, not plain blank line"
993 );
994 }
995
996 #[test]
997 fn test_fix_preserves_nested_blockquote_prefix_for_table() {
998 let rule = MD058BlanksAroundTables::default();
1000
1001 let content = ">> Nested quote
1002>> | H1 |
1003>> |----|
1004>> | a |
1005>> # More text";
1006 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1007 let fixed = rule.fix(&ctx).unwrap();
1008
1009 let expected = ">> Nested quote
1011>>
1012>> | H1 |
1013>> |----|
1014>> | a |
1015>>
1016>> # More text";
1017 assert_eq!(fixed, expected, "Fix should preserve nested blockquote prefix '>>'");
1018 }
1019
1020 #[test]
1021 fn test_fix_preserves_triple_nested_blockquote_prefix_for_table() {
1022 let rule = MD058BlanksAroundTables::default();
1024
1025 let content = ">>> Triple nested
1026>>> | A | B |
1027>>> |---|---|
1028>>> | 1 | 2 |
1029>>> # More text";
1030 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1031 let fixed = rule.fix(&ctx).unwrap();
1032
1033 let expected = ">>> Triple nested
1034>>>
1035>>> | A | B |
1036>>> |---|---|
1037>>> | 1 | 2 |
1038>>>
1039>>> # More text";
1040 assert_eq!(
1041 fixed, expected,
1042 "Fix should preserve triple-nested blockquote prefix '>>>'"
1043 );
1044 }
1045
1046 #[test]
1053 fn test_is_blank_line_with_blockquote_continuation() {
1054 let rule = MD058BlanksAroundTables::default();
1056
1057 assert!(rule.is_blank_line(""));
1059 assert!(rule.is_blank_line(" "));
1060 assert!(rule.is_blank_line("\t"));
1061 assert!(rule.is_blank_line(" \t "));
1062
1063 assert!(rule.is_blank_line(">"));
1065 assert!(rule.is_blank_line("> "));
1066 assert!(rule.is_blank_line("> "));
1067 assert!(rule.is_blank_line(">>"));
1068 assert!(rule.is_blank_line(">> "));
1069 assert!(rule.is_blank_line(">>>"));
1070 assert!(rule.is_blank_line("> > "));
1071 assert!(rule.is_blank_line("> > > "));
1072 assert!(rule.is_blank_line(" > ")); assert!(!rule.is_blank_line("text"));
1076 assert!(!rule.is_blank_line("> text"));
1077 assert!(!rule.is_blank_line(">> text"));
1078 assert!(!rule.is_blank_line("> | table |"));
1079 assert!(!rule.is_blank_line("| table |"));
1080 }
1081
1082 #[test]
1083 fn test_issue_305_no_warning_blockquote_with_existing_blank_before_table() {
1084 let rule = MD058BlanksAroundTables::default();
1087
1088 let content = "> Text before
1089>
1090> | H1 | H2 |
1091> |----|---|
1092> | a | b |";
1093 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1094 let result = rule.check(&ctx).unwrap();
1095
1096 assert_eq!(
1097 result.len(),
1098 0,
1099 "Should not warn when blockquote already has blank line before table"
1100 );
1101 }
1102
1103 #[test]
1104 fn test_issue_305_no_warning_blockquote_with_existing_blank_after_table() {
1105 let rule = MD058BlanksAroundTables::default();
1108
1109 let content = "> | H1 | H2 |
1110> |----|---|
1111> | a | b |
1112>
1113> Text after";
1114 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1115 let result = rule.check(&ctx).unwrap();
1116
1117 assert_eq!(
1118 result.len(),
1119 0,
1120 "Should not warn when blockquote already has blank line after table"
1121 );
1122 }
1123
1124 #[test]
1125 fn test_issue_305_no_warning_blockquote_with_both_blank_lines() {
1126 let rule = MD058BlanksAroundTables::default();
1128
1129 let content = "> The following options are available:
1130>
1131> | Option | Default | Description |
1132> |--------|-----------|-------------------|
1133> | port | 3000 | Server port |
1134> | host | localhost | Server host |";
1135 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1136 let result = rule.check(&ctx).unwrap();
1137
1138 assert_eq!(
1139 result.len(),
1140 0,
1141 "Issue #305: Should not warn for valid table inside blockquote with blank line"
1142 );
1143 }
1144
1145 #[test]
1146 fn test_issue_305_no_warning_nested_blockquote_with_blank_lines() {
1147 let rule = MD058BlanksAroundTables::default();
1149
1150 let content = ">> Nested text
1151>>
1152>> | Col1 | Col2 |
1153>> |------|------|
1154>> | val1 | val2 |
1155>>
1156>> More text";
1157 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1158 let result = rule.check(&ctx).unwrap();
1159
1160 assert_eq!(
1161 result.len(),
1162 0,
1163 "Should not warn for nested blockquote table with blank lines"
1164 );
1165 }
1166
1167 #[test]
1168 fn test_issue_305_no_warning_triple_nested_blockquote_with_blank_lines() {
1169 let rule = MD058BlanksAroundTables::default();
1171
1172 let content = ">>> Deep nesting
1173>>>
1174>>> | A | B |
1175>>> |---|---|
1176>>> | 1 | 2 |
1177>>>
1178>>> End";
1179 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1180 let result = rule.check(&ctx).unwrap();
1181
1182 assert_eq!(
1183 result.len(),
1184 0,
1185 "Should not warn for triple-nested blockquote table with blank lines"
1186 );
1187 }
1188
1189 #[test]
1190 fn test_issue_305_fix_does_not_corrupt_valid_blockquote_table() {
1191 let rule = MD058BlanksAroundTables::default();
1193
1194 let content = "> Text before
1195>
1196> | H1 | H2 |
1197> |----|---|
1198> | a | b |
1199>
1200> Text after";
1201 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1202 let fixed = rule.fix(&ctx).unwrap();
1203
1204 assert_eq!(fixed, content, "Fix should not modify already-valid blockquote table");
1205 }
1206
1207 #[test]
1208 fn test_issue_305_blockquote_blank_with_trailing_space() {
1209 let rule = MD058BlanksAroundTables::default();
1211
1212 let content = "> Text before
1214>
1215> | H1 | H2 |
1216> |----|---|
1217> | a | b |";
1218 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1219 let result = rule.check(&ctx).unwrap();
1220
1221 assert_eq!(
1222 result.len(),
1223 0,
1224 "Should recognize '> ' (with trailing space) as blank line"
1225 );
1226 }
1227
1228 #[test]
1229 fn test_issue_305_spaced_nested_blockquote() {
1230 let rule = MD058BlanksAroundTables::default();
1232
1233 let content = "> > Nested text
1234> >
1235> > | H1 |
1236> > |----|
1237> > | a |";
1238 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1239 let result = rule.check(&ctx).unwrap();
1240
1241 assert_eq!(
1242 result.len(),
1243 0,
1244 "Should recognize '> > ' style nested blockquote blank line"
1245 );
1246 }
1247
1248 #[test]
1249 fn test_mixed_regular_and_blockquote_tables() {
1250 let rule = MD058BlanksAroundTables::default();
1252
1253 let content = "# Mixed Content
1254
1255Regular table:
1256
1257| A | B |
1258|---|---|
1259| 1 | 2 |
1260
1261And a blockquote table:
1262
1263> Quote text
1264>
1265> | X | Y |
1266> |---|---|
1267> | 3 | 4 |
1268>
1269> End quote
1270
1271Final paragraph.";
1272 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1273 let result = rule.check(&ctx).unwrap();
1274
1275 assert_eq!(
1276 result.len(),
1277 0,
1278 "Should handle mixed regular and blockquote tables correctly"
1279 );
1280 }
1281
1282 #[test]
1283 fn test_blockquote_table_at_document_start() {
1284 let rule = MD058BlanksAroundTables::default();
1286
1287 let content = "> | H1 | H2 |
1288> |----|---|
1289> | a | b |
1290>
1291> Text after";
1292 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1293 let result = rule.check(&ctx).unwrap();
1294
1295 assert_eq!(
1296 result.len(),
1297 0,
1298 "Should not require blank line before table at document start (even in blockquote)"
1299 );
1300 }
1301
1302 #[test]
1303 fn test_blockquote_table_at_document_end() {
1304 let rule = MD058BlanksAroundTables::default();
1306
1307 let content = "> Text before
1308>
1309> | H1 | H2 |
1310> |----|---|
1311> | a | b |";
1312 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1313 let result = rule.check(&ctx).unwrap();
1314
1315 assert_eq!(
1316 result.len(),
1317 0,
1318 "Should not require blank line after table at document end"
1319 );
1320 }
1321
1322 #[test]
1323 fn test_blockquote_table_missing_blank_still_detected() {
1324 let rule = MD058BlanksAroundTables::default();
1326
1327 let content = "> Text before
1328> | H1 | H2 |
1329> |----|---|
1330> | a | b |
1331> # Text after";
1332 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1333 let result = rule.check(&ctx).unwrap();
1334
1335 assert_eq!(
1337 result.len(),
1338 2,
1339 "Should still detect missing blank lines in blockquote tables"
1340 );
1341 assert!(result[0].message.contains("before table"));
1342 assert!(result[1].message.contains("after table"));
1343 }
1344
1345 #[test]
1346 fn test_blockquote_table_fix_adds_correct_prefix() {
1347 let rule = MD058BlanksAroundTables::default();
1349
1350 let content = "> Text before
1351> | H1 | H2 |
1352> |----|---|
1353> | a | b |
1354> # Text after";
1355 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1356 let fixed = rule.fix(&ctx).unwrap();
1357
1358 let expected = "> Text before
1359>
1360> | H1 | H2 |
1361> |----|---|
1362> | a | b |
1363>
1364> # Text after";
1365 assert_eq!(fixed, expected, "Fix should add blockquote-prefixed blank lines");
1366 }
1367
1368 #[test]
1369 fn test_multiple_blockquote_tables_with_valid_spacing() {
1370 let rule = MD058BlanksAroundTables::default();
1372
1373 let content = "> First table:
1374>
1375> | A | B |
1376> |---|---|
1377> | 1 | 2 |
1378>
1379> Second table:
1380>
1381> | X | Y |
1382> |---|---|
1383> | 3 | 4 |
1384>
1385> End";
1386 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1387 let result = rule.check(&ctx).unwrap();
1388
1389 assert_eq!(
1390 result.len(),
1391 0,
1392 "Should handle multiple blockquote tables with valid spacing"
1393 );
1394 }
1395
1396 #[test]
1397 fn test_blockquote_table_with_minimum_before_config() {
1398 let config = MD058Config {
1400 minimum_before: 2,
1401 minimum_after: 1,
1402 };
1403 let rule = MD058BlanksAroundTables::from_config_struct(config);
1404
1405 let content = "> Text
1406>
1407> | H1 |
1408> |----|
1409> | a |";
1410 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1411 let result = rule.check(&ctx).unwrap();
1412
1413 assert_eq!(result.len(), 1);
1415 assert!(result[0].message.contains("before table"));
1416 }
1417
1418 #[test]
1434 fn md058_pandoc_grid_tables_not_flagged() {
1435 let rule = MD058BlanksAroundTables::default();
1436 let content = "Some text before.
1439+---+---+
1440| a | b |
1441+===+===+
1442| 1 | 2 |
1443+---+---+
1444Some text after.";
1445
1446 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1448 let result = rule.check(&ctx).unwrap();
1449 assert!(
1450 result.is_empty(),
1451 "MD058 should not flag blank lines around Pandoc grid tables (excluded by table_blocks): {result:?}"
1452 );
1453
1454 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1456 let result_std = rule.check(&ctx_std).unwrap();
1457 assert!(
1458 result_std.is_empty(),
1459 "MD058 should not flag grid-table-like content under Standard: {result_std:?}"
1460 );
1461 }
1462
1463 #[test]
1464 fn md058_pandoc_multi_line_tables_not_flagged() {
1465 let rule = MD058BlanksAroundTables::default();
1466 let content = "Some text.
1467--------- -----------
1468Header 1 Header 2
1469--------- -----------
1470Cell 1 Cell 2
1471--------- -----------
1472More text.";
1473
1474 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1475 let result = rule.check(&ctx).unwrap();
1476 assert!(
1477 result.is_empty(),
1478 "MD058 should not flag Pandoc multi-line tables: {result:?}"
1479 );
1480
1481 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1482 let result_std = rule.check(&ctx_std).unwrap();
1483 assert!(
1484 result_std.is_empty(),
1485 "MD058 should not flag multi-line table content under Standard: {result_std:?}"
1486 );
1487 }
1488
1489 #[test]
1490 fn md058_pandoc_line_blocks_not_flagged() {
1491 let rule = MD058BlanksAroundTables::default();
1492 let content = "Some text.
1494| First line
1495| Second line
1496More text.";
1497
1498 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1499 let result = rule.check(&ctx).unwrap();
1500 assert!(
1501 result.is_empty(),
1502 "MD058 should not treat Pandoc line blocks as tables: {result:?}"
1503 );
1504
1505 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1506 let result_std = rule.check(&ctx_std).unwrap();
1507 assert!(
1508 result_std.is_empty(),
1509 "MD058 should not treat line-block-like content as tables under Standard: {result_std:?}"
1510 );
1511 }
1512
1513 #[test]
1514 fn md058_pandoc_pipe_table_captions_not_flagged() {
1515 let rule = MD058BlanksAroundTables::default();
1516 let content = "\
1519Some text.
1520
1521| H1 | H2 |
1522|----|-----|
1523| a | b |
1524
1525: My table caption
1526More text.";
1527
1528 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1529 let result = rule.check(&ctx).unwrap();
1530 assert!(
1531 result.is_empty(),
1532 "MD058 should not flag the pipe-table caption line as needing blank lines: {result:?}"
1533 );
1534
1535 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1538 let result_std = rule.check(&ctx_std).unwrap();
1539 assert!(
1540 result_std.is_empty(),
1541 "MD058 table with caption — caption not a table row under Standard: {result_std:?}"
1542 );
1543 }
1544
1545 #[test]
1546 fn md058_hugo_block_attribute_after_table_not_flagged() {
1547 let rule = MD058BlanksAroundTables::default();
1550 let content = "\
1551Some text.
1552
1553| H1 | H2 |
1554|----|-----|
1555| a | b |
1556{class=\"table table-striped\"}
1557
1558More text.";
1559
1560 for flavor in [
1561 crate::config::MarkdownFlavor::Hugo,
1562 crate::config::MarkdownFlavor::MkDocs,
1563 crate::config::MarkdownFlavor::Kramdown,
1564 ] {
1565 let ctx = LintContext::new(content, flavor, None);
1566 let result = rule.check(&ctx).unwrap();
1567 assert!(
1568 result.is_empty(),
1569 "MD058 should not flag the block attribute line under {flavor:?}: {result:?}"
1570 );
1571 }
1572
1573 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1576 let result_std = rule.check(&ctx_std).unwrap();
1577 assert!(
1578 result_std.is_empty(),
1579 "the attribute line is a table row under Standard: {result_std:?}"
1580 );
1581 }
1582
1583 #[test]
1584 fn test_fix_preserves_trailing_newline() {
1585 let rule = MD058BlanksAroundTables::default();
1586
1587 let content = "Intro\n| a | b |\n| --- | --- |\n| 1 | 2 |\n# After\n";
1588 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1589 let fixed = rule.fix(&ctx).unwrap();
1590
1591 assert!(fixed.ends_with('\n'), "Fix should preserve trailing newline");
1592 assert_eq!(fixed, "Intro\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n\n# After\n");
1593 }
1594
1595 #[test]
1596 fn test_fix_preserves_no_trailing_newline() {
1597 let rule = MD058BlanksAroundTables::default();
1598
1599 let content = "Intro\n| a | b |\n| --- | --- |\n| 1 | 2 |\n# After";
1600 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1601 let fixed = rule.fix(&ctx).unwrap();
1602
1603 assert!(
1604 !fixed.ends_with('\n'),
1605 "Fix should not add trailing newline if original didn't have one"
1606 );
1607 assert_eq!(fixed, "Intro\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n\n# After");
1608 }
1609
1610 #[test]
1611 fn test_indented_lazy_rows_in_blockquote_are_not_a_table() {
1612 let rule = MD058BlanksAroundTables::default();
1613 for content in [
1614 "> o\n> |a|b|\n> |-|-|\n",
1615 ">o\n> |a|b|\n> |-|-|\n",
1616 "> > o\n> > |a|b|\n> > |-|-|\n",
1617 "> o\n> |a|b|\n> |-|-|\n",
1618 ] {
1619 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1620 assert!(rule.check(&ctx).unwrap().is_empty(), "{content:?}");
1621 assert_eq!(rule.fix(&ctx).unwrap(), content, "{content:?}");
1622 }
1623 }
1624
1625 #[test]
1626 fn test_real_table_after_lazy_rows_in_blockquote_is_still_checked() {
1627 let rule = MD058BlanksAroundTables::default();
1628 let content = "> o\n> |a|b|\n> |-|-|\n> |c|d|\n> |-|-|\n";
1629 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1630 assert_eq!(
1631 rule.fix(&ctx).unwrap(),
1632 "> o\n> |a|b|\n> |-|-|\n>\n> |c|d|\n> |-|-|\n"
1633 );
1634 }
1635
1636 #[test]
1637 fn test_admonition_first_body_line_table_needs_no_blank_before() {
1638 let rule = MD058BlanksAroundTables::default();
1639 for opener in ["!!! note", "??? note", "???+ note", "!!! note \"Title\"", " !!! note"] {
1640 let indent = if opener.starts_with(" ") { " " } else { " " };
1641 let content = format!("{opener}\n{indent}| a | b |\n{indent}|---|---|\n{indent}| 1 | 2 |\n\nafter\n");
1642 let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::MkDocs, None);
1643 assert!(
1644 rule.check(&ctx).unwrap().is_empty(),
1645 "table opening an admonition body is not missing a blank line: {content:?}"
1646 );
1647 assert_eq!(rule.fix(&ctx).unwrap(), content, "fix must not touch {content:?}");
1648 }
1649 }
1650
1651 #[test]
1652 fn test_admonition_table_after_body_text_still_flagged() {
1653 let rule = MD058BlanksAroundTables::default();
1654 let content = "!!! note\n\n text\n | a | b |\n |---|---|\n | 1 | 2 |\n";
1656 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1657 let result = rule.check(&ctx).unwrap();
1658 assert!(result.iter().any(|w| w.message.contains("before table")), "{result:?}");
1659 }
1660}