1use crate::rule::{LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::utils::range_utils::calculate_line_range;
3use crate::utils::table_utils::{TableBlock, TableUtils};
4
5mod md055_config;
6use md055_config::MD055Config;
7
8#[derive(Debug, Default, Clone)]
81pub struct MD055TablePipeStyle {
82 config: MD055Config,
83}
84
85impl MD055TablePipeStyle {
86 pub fn new(style: String) -> Self {
87 Self {
88 config: MD055Config { style },
89 }
90 }
91
92 pub fn from_config_struct(config: MD055Config) -> Self {
93 Self { config }
94 }
95
96 fn determine_table_style(&self, table_block: &TableBlock, lines: &[&str]) -> Option<&'static str> {
98 let mut leading_and_trailing_count = 0;
99 let mut no_leading_or_trailing_count = 0;
100 let mut leading_only_count = 0;
101 let mut trailing_only_count = 0;
102
103 let header_content = TableUtils::extract_table_row_content(lines[table_block.header_line], table_block, 0);
105 if let Some(style) = TableUtils::determine_pipe_style(header_content) {
106 match style {
107 "leading_and_trailing" => leading_and_trailing_count += 1,
108 "no_leading_or_trailing" => no_leading_or_trailing_count += 1,
109 "leading_only" => leading_only_count += 1,
110 "trailing_only" => trailing_only_count += 1,
111 _ => {}
112 }
113 }
114
115 for (i, &line_idx) in table_block.content_lines.iter().enumerate() {
117 let content = TableUtils::extract_table_row_content(lines[line_idx], table_block, 2 + i);
118 if let Some(style) = TableUtils::determine_pipe_style(content) {
119 match style {
120 "leading_and_trailing" => leading_and_trailing_count += 1,
121 "no_leading_or_trailing" => no_leading_or_trailing_count += 1,
122 "leading_only" => leading_only_count += 1,
123 "trailing_only" => trailing_only_count += 1,
124 _ => {}
125 }
126 }
127 }
128
129 let max_count = leading_and_trailing_count
132 .max(no_leading_or_trailing_count)
133 .max(leading_only_count)
134 .max(trailing_only_count);
135
136 if max_count > 0 {
137 if leading_and_trailing_count == max_count {
138 Some("leading_and_trailing")
139 } else if no_leading_or_trailing_count == max_count {
140 Some("no_leading_or_trailing")
141 } else if leading_only_count == max_count {
142 Some("leading_only")
143 } else if trailing_only_count == max_count {
144 Some("trailing_only")
145 } else {
146 None
147 }
148 } else {
149 None
150 }
151 }
152
153 #[cfg(test)]
155 fn fix_table_row(&self, line: &str, target_style: &str) -> String {
156 let dummy_block = TableBlock {
157 start_line: 0,
158 end_line: 0,
159 header_line: 0,
160 delimiter_line: 0,
161 content_lines: vec![],
162 list_context: None,
163 };
164 self.fix_table_row_with_context(line, target_style, &dummy_block, 0)
165 }
166
167 fn fix_table_row_with_context(
172 &self,
173 line: &str,
174 target_style: &str,
175 table_block: &TableBlock,
176 table_line_index: usize,
177 ) -> String {
178 let (bq_prefix, after_bq) = TableUtils::extract_blockquote_prefix(line);
180
181 if let Some(ref list_ctx) = table_block.list_context {
183 if table_line_index == 0 {
184 let stripped = after_bq
186 .strip_prefix(&list_ctx.list_prefix)
187 .unwrap_or_else(|| TableUtils::extract_list_prefix(after_bq).1);
188 let fixed_content = self.fix_table_content(stripped.trim(), target_style);
189
190 let lp = &list_ctx.list_prefix;
192 if bq_prefix.is_empty() && lp.is_empty() {
193 fixed_content
194 } else {
195 format!("{bq_prefix}{lp}{fixed_content}")
196 }
197 } else {
198 let content_indent = list_ctx.content_indent;
200 let stripped = TableUtils::extract_table_row_content(line, table_block, table_line_index);
201 let fixed_content = self.fix_table_content(stripped.trim(), target_style);
202
203 let indent = " ".repeat(content_indent);
205 format!("{bq_prefix}{indent}{fixed_content}")
206 }
207 } else {
208 let fixed_content = self.fix_table_content(after_bq.trim(), target_style);
210 if bq_prefix.is_empty() {
211 fixed_content
212 } else {
213 format!("{bq_prefix}{fixed_content}")
214 }
215 }
216 }
217
218 fn fix_table_content(&self, trimmed: &str, target_style: &str) -> String {
220 if !trimmed.contains('|') {
221 return trimmed.to_string();
222 }
223
224 let has_leading = trimmed.starts_with('|');
225 let has_trailing = trimmed.ends_with('|');
226
227 match target_style {
228 "leading_and_trailing" => {
229 let mut result = trimmed.to_string();
230
231 if !has_leading {
233 result = format!("| {result}");
234 }
235
236 if !has_trailing {
238 result = format!("{result} |");
239 }
240
241 result
242 }
243 "no_leading_or_trailing" => {
244 let mut result = trimmed;
245
246 if has_leading {
248 result = result.strip_prefix('|').unwrap_or(result);
249 result = result.trim_start();
250 }
251
252 if has_trailing {
254 result = result.strip_suffix('|').unwrap_or(result);
255 result = result.trim_end();
256 }
257
258 result.to_string()
259 }
260 "leading_only" => {
261 let mut result = trimmed.to_string();
262
263 if !has_leading {
265 result = format!("| {result}");
266 }
267
268 if has_trailing {
270 result = result.strip_suffix('|').unwrap_or(&result).trim_end().to_string();
271 }
272
273 result
274 }
275 "trailing_only" => {
276 let mut result = trimmed;
277
278 if has_leading {
280 result = result.strip_prefix('|').unwrap_or(result).trim_start();
281 }
282
283 let mut result = result.to_string();
284
285 if !has_trailing {
287 result = format!("{result} |");
288 }
289
290 result
291 }
292 _ => trimmed.to_string(),
293 }
294 }
295}
296
297impl Rule for MD055TablePipeStyle {
298 fn name(&self) -> &'static str {
299 "MD055"
300 }
301
302 fn description(&self) -> &'static str {
303 "Table pipe style should be consistent"
304 }
305
306 fn category(&self) -> RuleCategory {
307 RuleCategory::Table
308 }
309
310 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
311 !ctx.likely_has_tables()
313 }
314
315 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
316 let mut warnings = Vec::new();
317
318 let lines = ctx.raw_lines();
321
322 let configured_style = match self.config.style.as_str() {
324 "leading_and_trailing" | "no_leading_or_trailing" | "leading_only" | "trailing_only" | "consistent" => {
325 self.config.style.as_str()
326 }
327 _ => {
328 "leading_and_trailing"
330 }
331 };
332
333 let table_blocks = &ctx.table_blocks;
335
336 for table_block in table_blocks {
338 let table_style = if configured_style == "consistent" {
341 self.determine_table_style(table_block, lines)
342 } else {
343 None
344 };
345
346 let target_style = if configured_style == "consistent" {
348 table_style.unwrap_or("leading_and_trailing")
349 } else {
350 configured_style
351 };
352
353 let all_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
355 .chain(std::iter::once(table_block.delimiter_line))
356 .chain(table_block.content_lines.iter().copied())
357 .collect();
358
359 for (table_line_idx, &line_idx) in all_line_indices.iter().enumerate() {
363 let line = lines[line_idx];
364 let content = TableUtils::extract_table_row_content(line, table_block, table_line_idx);
366 if let Some(current_style) = TableUtils::determine_pipe_style(content) {
367 let needs_fixing = current_style != target_style;
369
370 if needs_fixing {
371 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_idx + 1, line);
372
373 let message = format!(
374 "Table pipe style should be {}",
375 match target_style {
376 "leading_and_trailing" => "leading and trailing",
377 "no_leading_or_trailing" => "no leading or trailing",
378 "leading_only" => "leading only",
379 "trailing_only" => "trailing only",
380 _ => target_style,
381 }
382 );
383
384 let fixed_line =
387 self.fix_table_row_with_context(line, target_style, table_block, table_line_idx);
388 let row_range = ctx.line_column_byte_range_with_length(line_idx + 1, 1, line.chars().count());
389
390 warnings.push(LintWarning {
391 rule_name: Some(self.name().to_string()),
392 severity: Severity::Warning,
393 message,
394 line: start_line,
395 column: start_col,
396 end_line,
397 end_column: end_col,
398 fix: Some(crate::rule::Fix::new(row_range, fixed_line)),
399 });
400 }
401 }
402 }
403 }
404
405 Ok(warnings)
406 }
407
408 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
409 if self.should_skip(ctx) {
410 return Ok(ctx.content.to_string());
411 }
412 let warnings = self.check(ctx)?;
413 if warnings.is_empty() {
414 return Ok(ctx.content.to_string());
415 }
416 let warnings =
417 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
418 crate::utils::fix_utils::apply_warning_fixes(ctx.content, &warnings).map_err(LintError::InvalidInput)
419 }
420
421 fn as_any(&self) -> &dyn std::any::Any {
422 self
423 }
424
425 crate::impl_rule_config_methods!(MD055Config);
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 fn rule_from_toml_style(style: &str) -> MD055TablePipeStyle {
438 let config: md055_config::MD055Config =
439 toml::from_str(&format!("style = \"{style}\"")).expect("valid style value");
440 MD055TablePipeStyle::from_config_struct(config)
441 }
442
443 #[test]
444 fn test_no_leading_or_trailing_kebab_accepts_conforming_table() {
445 let rule = rule_from_toml_style("no-leading-or-trailing");
446 let content = "A | B\n--- | ---\n1 | 2";
447 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
448 let warnings = rule.check(&ctx).unwrap();
449 assert!(
450 warnings.is_empty(),
451 "no-leading-or-trailing should accept a table with no pipes: {warnings:?}"
452 );
453 }
454
455 #[test]
456 fn test_no_leading_or_trailing_kebab_rejects_nonconforming_table() {
457 let rule = rule_from_toml_style("no-leading-or-trailing");
458 let content = "| A | B |\n|---|---|\n| 1 | 2 |";
459 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
460 let warnings = rule.check(&ctx).unwrap();
461 assert_eq!(
462 warnings.len(),
463 3,
464 "no-leading-or-trailing should flag all 3 rows with pipes"
465 );
466 assert!(warnings.iter().all(|w| w.message.contains("no leading or trailing")));
467 }
468
469 #[test]
470 fn test_leading_only_kebab_accepts_conforming_table() {
471 let rule = rule_from_toml_style("leading-only");
472 let content = "| A | B\n|---|---\n| 1 | 2";
473 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
474 let warnings = rule.check(&ctx).unwrap();
475 assert!(
476 warnings.is_empty(),
477 "leading-only should accept a leading-only table: {warnings:?}"
478 );
479 }
480
481 #[test]
482 fn test_trailing_only_kebab_accepts_conforming_table() {
483 let rule = rule_from_toml_style("trailing-only");
484 let content = "A | B |\n---|---|\n1 | 2 |";
485 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
486 let warnings = rule.check(&ctx).unwrap();
487 assert!(
488 warnings.is_empty(),
489 "trailing-only should accept a trailing-only table: {warnings:?}"
490 );
491 }
492
493 #[test]
494 fn test_trailing_only_kebab_rejects_nonconforming_table() {
495 let rule = rule_from_toml_style("trailing-only");
496 let content = "| A | B |\n|---|---|\n| 1 | 2 |";
498 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
499 let warnings = rule.check(&ctx).unwrap();
500 assert_eq!(
501 warnings.len(),
502 3,
503 "trailing-only should flag all 3 rows that have leading pipes"
504 );
505 assert!(warnings.iter().all(|w| w.message.contains("trailing only")));
506 }
507
508 #[test]
509 fn test_leading_only_kebab_rejects_nonconforming_table() {
510 let rule = rule_from_toml_style("leading-only");
511 let content = "A | B |\n---|---|\n1 | 2 |";
513 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
514 let warnings = rule.check(&ctx).unwrap();
515 assert_eq!(
516 warnings.len(),
517 3,
518 "leading-only should flag all 3 rows that have trailing pipes"
519 );
520 assert!(warnings.iter().all(|w| w.message.contains("leading only")));
521 }
522
523 #[test]
524 fn test_leading_and_trailing_kebab_accepts_conforming_table() {
525 let rule = rule_from_toml_style("leading-and-trailing");
526 let content = "| A | B |\n|---|---|\n| 1 | 2 |";
527 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
528 let warnings = rule.check(&ctx).unwrap();
529 assert!(
530 warnings.is_empty(),
531 "leading-and-trailing should accept a fully-piped table: {warnings:?}"
532 );
533 }
534
535 #[test]
536 fn test_kebab_and_snake_case_styles_are_equivalent() {
537 let pairs = [
540 ("no-leading-or-trailing", "no_leading_or_trailing"),
541 ("leading-only", "leading_only"),
542 ("trailing-only", "trailing_only"),
543 ("leading-and-trailing", "leading_and_trailing"),
544 ];
545 let content = "| A | B |\n|---|---|\n| 1 | 2 |";
547 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
548
549 for (kebab, snake) in pairs {
550 let kebab_rule = rule_from_toml_style(kebab);
551 let snake_rule = rule_from_toml_style(snake);
552 let kebab_warnings = kebab_rule.check(&ctx).unwrap();
553 let snake_warnings = snake_rule.check(&ctx).unwrap();
554
555 assert_eq!(
556 kebab_warnings.len(),
557 snake_warnings.len(),
558 "'{kebab}' and '{snake}' must produce the same number of warnings"
559 );
560 for (i, (kw, sw)) in kebab_warnings.iter().zip(snake_warnings.iter()).enumerate() {
561 assert_eq!(
562 kw.message, sw.message,
563 "warning[{i}] message differs between '{kebab}' and '{snake}'"
564 );
565 assert_eq!(
566 kw.line, sw.line,
567 "warning[{i}] line differs between '{kebab}' and '{snake}'"
568 );
569 }
570 }
571 }
572
573 fn assert_fix_roundtrip_from_toml(style: &str, content: &str) {
574 let rule = rule_from_toml_style(style);
575 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
576 let fixed = rule.fix(&ctx).unwrap();
577 let ctx2 = crate::lint_context::LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
578 let remaining = rule.check(&ctx2).unwrap();
579 assert!(
580 remaining.is_empty(),
581 "style '{style}': after fix(), check() should find 0 violations.\n\
582 Original: {content:?}\n\
583 Fixed: {fixed:?}\n\
584 Remaining: {remaining:?}"
585 );
586 }
587
588 #[test]
589 fn test_roundtrip_kebab_no_leading_or_trailing() {
590 assert_fix_roundtrip_from_toml("no-leading-or-trailing", "| H1 | H2 |\n|---|---|\n| a | b |");
591 }
592
593 #[test]
594 fn test_roundtrip_kebab_leading_and_trailing() {
595 assert_fix_roundtrip_from_toml("leading-and-trailing", "H1 | H2\n---|---\na | b");
596 }
597
598 #[test]
599 fn test_roundtrip_kebab_leading_only() {
600 assert_fix_roundtrip_from_toml("leading-only", "| H1 | H2 |\n|---|---|\n| a | b |");
601 }
602
603 #[test]
604 fn test_roundtrip_kebab_trailing_only() {
605 assert_fix_roundtrip_from_toml("trailing-only", "| H1 | H2 |\n|---|---|\n| a | b |");
606 }
607
608 #[test]
609 fn test_md055_delimiter_row_handling() {
610 let rule = MD055TablePipeStyle::new("no_leading_or_trailing".to_string());
612
613 let content = "| Header 1 | Header 2 | Header 3 |\n|----------|----------|----------|\n| Data 1 | Data 2 | Data 3 |";
614 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
615 let result = rule.fix(&ctx).unwrap();
616
617 let expected = "Header 1 | Header 2 | Header 3\n----------|----------|----------\nData 1 | Data 2 | Data 3";
620
621 assert_eq!(result, expected);
622
623 let warnings = rule.check(&ctx).unwrap();
625 let delimiter_warning = &warnings[1]; assert_eq!(delimiter_warning.line, 2);
627 assert_eq!(
628 delimiter_warning.message,
629 "Table pipe style should be no leading or trailing"
630 );
631
632 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
634
635 let content = "Header 1 | Header 2 | Header 3\n----------|----------|----------\nData 1 | Data 2 | Data 3";
636 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
637 let result = rule.fix(&ctx).unwrap();
638
639 let expected = "| Header 1 | Header 2 | Header 3 |\n| ----------|----------|---------- |\n| Data 1 | Data 2 | Data 3 |";
642
643 assert_eq!(result, expected);
644 }
645
646 #[test]
647 fn test_md055_check_finds_delimiter_row_issues() {
648 let rule = MD055TablePipeStyle::new("no_leading_or_trailing".to_string());
650
651 let content = "| Header 1 | Header 2 | Header 3 |\n|----------|----------|----------|\n| Data 1 | Data 2 | Data 3 |";
652 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
653 let warnings = rule.check(&ctx).unwrap();
654
655 assert_eq!(warnings.len(), 3);
657
658 let delimiter_warning = &warnings[1];
660 assert_eq!(delimiter_warning.line, 2);
661 assert_eq!(
662 delimiter_warning.message,
663 "Table pipe style should be no leading or trailing"
664 );
665 }
666
667 #[test]
668 fn test_md055_real_world_example() {
669 let rule = MD055TablePipeStyle::new("no_leading_or_trailing".to_string());
671
672 let content = "# Table Example\n\nHere's a table with leading and trailing pipes:\n\n| Header 1 | Header 2 | Header 3 |\n|----------|----------|----------|\n| Data 1 | Data 2 | Data 3 |\n| Data 4 | Data 5 | Data 6 |\n\nMore content after the table.";
673 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
674 let result = rule.fix(&ctx).unwrap();
675
676 let expected = "# Table Example\n\nHere's a table with leading and trailing pipes:\n\nHeader 1 | Header 2 | Header 3\n----------|----------|----------\nData 1 | Data 2 | Data 3\nData 4 | Data 5 | Data 6\n\nMore content after the table.";
679
680 assert_eq!(result, expected);
681
682 let warnings = rule.check(&ctx).unwrap();
684 assert_eq!(warnings.len(), 4); assert_eq!(warnings[0].line, 5); assert_eq!(warnings[1].line, 6); assert_eq!(warnings[2].line, 7); assert_eq!(warnings[3].line, 8); }
692
693 #[test]
694 fn test_md055_invalid_style() {
695 let rule = MD055TablePipeStyle::new("leading_or_trailing".to_string()); let content = "| Header 1 | Header 2 | Header 3 |\n|----------|----------|----------|\n| Data 1 | Data 2 | Data 3 |";
699 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
700 let result = rule.fix(&ctx).unwrap();
701
702 let expected = "| Header 1 | Header 2 | Header 3 |\n|----------|----------|----------|\n| Data 1 | Data 2 | Data 3 |";
705
706 assert_eq!(result, expected);
707
708 let content = "Header 1 | Header 2 | Header 3\n----------|----------|----------\nData 1 | Data 2 | Data 3";
710 let ctx2 = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
711 let result = rule.fix(&ctx2).unwrap();
712
713 let expected = "| Header 1 | Header 2 | Header 3 |\n| ----------|----------|---------- |\n| Data 1 | Data 2 | Data 3 |";
716 assert_eq!(result, expected);
717
718 let warnings = rule.check(&ctx2).unwrap();
720
721 assert_eq!(warnings.len(), 3);
724 }
725
726 #[test]
727 fn test_underflow_protection() {
728 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
730
731 let result = rule.fix_table_row("", "leading_and_trailing");
733 assert_eq!(result, "");
734
735 let result = rule.fix_table_row("no pipes here", "leading_and_trailing");
737 assert_eq!(result, "no pipes here");
738
739 let result = rule.fix_table_row("|", "leading_and_trailing");
741 assert!(!result.is_empty());
743 }
744
745 #[test]
748 fn test_fix_table_row_in_blockquote() {
749 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
750
751 let result = rule.fix_table_row("> H1 | H2", "leading_and_trailing");
753 assert_eq!(result, "> | H1 | H2 |");
754
755 let result = rule.fix_table_row("> | H1 | H2 |", "leading_and_trailing");
757 assert_eq!(result, "> | H1 | H2 |");
758
759 let result = rule.fix_table_row("> | H1 | H2 |", "no_leading_or_trailing");
761 assert_eq!(result, "> H1 | H2");
762 }
763
764 #[test]
765 fn test_fix_table_row_in_nested_blockquote() {
766 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
767
768 let result = rule.fix_table_row(">> H1 | H2", "leading_and_trailing");
770 assert_eq!(result, ">> | H1 | H2 |");
771
772 let result = rule.fix_table_row(">>> H1 | H2", "leading_and_trailing");
774 assert_eq!(result, ">>> | H1 | H2 |");
775 }
776
777 #[test]
778 fn test_blockquote_table_full_document() {
779 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
780
781 let content = "> H1 | H2\n> ----|----\n> a | b";
783 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
784 let result = rule.fix(&ctx).unwrap();
785
786 assert!(
789 result.starts_with("> |"),
790 "Header should start with blockquote + pipe. Got:\n{result}"
791 );
792 assert!(
794 result.contains("> | ----"),
795 "Delimiter should have blockquote prefix + leading pipe. Got:\n{result}"
796 );
797 }
798
799 #[test]
800 fn test_blockquote_table_no_leading_trailing() {
801 let rule = MD055TablePipeStyle::new("no_leading_or_trailing".to_string());
802
803 let content = "> | H1 | H2 |\n> |----|----|---|\n> | a | b |";
805 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
806 let result = rule.fix(&ctx).unwrap();
807
808 let lines: Vec<&str> = result.lines().collect();
810 assert!(lines[0].starts_with("> "), "Line should start with blockquote prefix");
811 assert!(
812 !lines[0].starts_with("> |"),
813 "Leading pipe should be removed. Got: {}",
814 lines[0]
815 );
816 }
817
818 #[test]
819 fn test_mixed_regular_and_blockquote_tables() {
820 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
821
822 let content = "H1 | H2\n---|---\na | b\n\n> H3 | H4\n> ---|---\n> c | d";
824 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
825 let result = rule.fix(&ctx).unwrap();
826
827 assert!(result.contains("| H1 | H2 |"), "Regular table should have pipes added");
829 assert!(
830 result.contains("> | H3 | H4 |"),
831 "Blockquote table should have pipes added with prefix preserved"
832 );
833 }
834
835 fn assert_fix_roundtrip(rule: &MD055TablePipeStyle, content: &str) {
838 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
839 let fixed = rule.fix(&ctx).unwrap();
840 let ctx2 = crate::lint_context::LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
841 let remaining = rule.check(&ctx2).unwrap();
842 assert!(
843 remaining.is_empty(),
844 "After fix(), check() should find 0 violations.\nOriginal: {content:?}\nFixed: {fixed:?}\nRemaining: {remaining:?}"
845 );
846 }
847
848 #[test]
849 fn test_roundtrip_leading_and_trailing() {
850 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
851 assert_fix_roundtrip(&rule, "H1 | H2\n---|---\na | b");
852 }
853
854 #[test]
855 fn test_roundtrip_no_leading_or_trailing() {
856 let rule = MD055TablePipeStyle::new("no_leading_or_trailing".to_string());
857 assert_fix_roundtrip(&rule, "| H1 | H2 |\n|---|---|\n| a | b |");
858 }
859
860 #[test]
861 fn test_roundtrip_consistent_mode() {
862 let rule = MD055TablePipeStyle::default();
863 assert_fix_roundtrip(&rule, "| H1 | H2 |\n|---|---|\nCell 1 | Cell 2");
864 }
865
866 #[test]
867 fn test_roundtrip_blockquote_table() {
868 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
869 assert_fix_roundtrip(&rule, "> H1 | H2\n> ---|---\n> a | b");
870 }
871
872 #[test]
873 fn test_roundtrip_mixed_tables() {
874 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
875 assert_fix_roundtrip(&rule, "H1 | H2\n---|---\na | b\n\n> H3 | H4\n> ---|---\n> c | d");
876 }
877
878 #[test]
879 fn test_roundtrip_with_surrounding_content() {
880 let rule = MD055TablePipeStyle::new("no_leading_or_trailing".to_string());
881 assert_fix_roundtrip(&rule, "# Title\n\n| H1 | H2 |\n|---|---|\n| a | b |\n\nMore text.");
882 }
883
884 #[test]
885 fn test_roundtrip_clean_content() {
886 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
887 assert_fix_roundtrip(&rule, "| H1 | H2 |\n|---|---|\n| a | b |");
888 }
889
890 #[test]
907 fn md055_pandoc_grid_tables_not_flagged() {
908 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
909 let content = "\
910+---+---+
911| a | b |
912+===+===+
913| 1 | 2 |
914+---+---+
915";
916 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
919 let result = rule.check(&ctx).unwrap();
920 assert!(
921 result.is_empty(),
922 "MD055 should not flag Pandoc grid tables (excluded by table_blocks): {result:?}"
923 );
924
925 let ctx_std = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
928 let result_std = rule.check(&ctx_std).unwrap();
929 assert!(
930 result_std.is_empty(),
931 "MD055 should not flag grid-table-like content under Standard either: {result_std:?}"
932 );
933 }
934
935 #[test]
936 fn md055_pandoc_multi_line_tables_not_flagged() {
937 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
938 let content = "\
940--------- ----------- ------
941Header 1 Header 2 Header 3
942--------- ----------- ------
943Cell 1 Cell 2 Cell 3
944--------- ----------- ------
945";
946 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
947 let result = rule.check(&ctx).unwrap();
948 assert!(
949 result.is_empty(),
950 "MD055 should not flag Pandoc multi-line tables: {result:?}"
951 );
952
953 let ctx_std = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
954 let result_std = rule.check(&ctx_std).unwrap();
955 assert!(
956 result_std.is_empty(),
957 "MD055 should not flag multi-line table content under Standard: {result_std:?}"
958 );
959 }
960
961 #[test]
962 fn md055_pandoc_line_blocks_not_flagged() {
963 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
964 let content = "| First line\n| Second line\n";
967 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
968 let result = rule.check(&ctx).unwrap();
969 assert!(
970 result.is_empty(),
971 "MD055 should not treat Pandoc line blocks as tables: {result:?}"
972 );
973
974 let ctx_std = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
975 let result_std = rule.check(&ctx_std).unwrap();
976 assert!(
977 result_std.is_empty(),
978 "MD055 should not treat line-block-like content as tables under Standard: {result_std:?}"
979 );
980 }
981
982 #[test]
983 fn md055_pandoc_pipe_table_captions_not_flagged() {
984 let rule = MD055TablePipeStyle::new("leading_and_trailing".to_string());
985 let content = "\
988| H1 | H2 |
989|----|-----|
990| a | b |
991
992: My table caption
993";
994 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
995 let result = rule.check(&ctx).unwrap();
996 assert!(
997 result.is_empty(),
998 "MD055 should not flag the pipe-table caption line: {result:?}"
999 );
1000
1001 let ctx_std = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1003 let result_std = rule.check(&ctx_std).unwrap();
1004 assert!(
1005 result_std.is_empty(),
1006 "MD055 already-valid table with caption should have no warnings under Standard: {result_std:?}"
1007 );
1008 }
1009}