use rumdl_lib::config::MarkdownFlavor;
use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::{ColumnAlign, MD013Config, MD060Config, MD060TableFormat};
use rumdl_lib::types::LineLength;
use unicode_width::UnicodeWidthStr;
#[test]
fn test_md060_align_simple_ascii_table() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 3, "Should warn about all three rows");
let fixed = rule.fix(&ctx).unwrap();
let expected = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
assert_eq!(fixed, expected);
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
}
#[test]
fn test_md060_cjk_characters() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Age |\n|---|---|\n| 中文 | 30 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("中文"), "CJK characters should be preserved");
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].width(), lines[1].width(), "Display widths should match");
assert_eq!(lines[1].width(), lines[2].width(), "Display widths should match");
let content2 = "| Name | City |\n|---|---|\n| Alice | 東京 |";
let ctx2 = LintContext::new(content2, MarkdownFlavor::Standard, None);
let fixed2 = rule.fix(&ctx2).unwrap();
assert!(fixed2.contains("東京"), "Japanese characters should be preserved");
let lines2: Vec<&str> = fixed2.lines().collect();
assert_eq!(lines2[0].width(), lines2[1].width(), "Display widths should match");
assert_eq!(lines2[1].width(), lines2[2].width(), "Display widths should match");
}
#[test]
fn test_md060_basic_emoji() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Status | Name |\n|---|---|\n| ✅ | Test |\n| ❌ | Fail |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("✅"), "Basic emoji should be preserved");
assert!(fixed.contains("❌"), "Basic emoji should be preserved");
assert!(fixed.contains("Test"));
assert!(fixed.contains("Fail"));
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 4);
assert_eq!(lines[0].width(), lines[1].width(), "Display widths should match");
assert_eq!(lines[1].width(), lines[2].width(), "Display widths should match");
assert_eq!(lines[2].width(), lines[3].width(), "Display widths should match");
}
#[test]
fn test_md060_zwj_emoji_skipped() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Emoji | Name |\n|---|---|\n| 👨👩👧👦 | Family |\n| 👩💻 | Developer |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(
warnings.len(),
0,
"Tables with ZWJ emoji should be skipped (no warnings)"
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "Tables with ZWJ emoji should not be modified");
}
#[test]
fn test_md060_inline_code_with_escaped_pipes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Pattern | Regex |\n|---|---|\n| Time | `[0-9]\\|[0-9]` |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains(r"`[0-9]\|[0-9]`"),
"Escaped pipes in inline code should be preserved"
);
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
}
#[test]
fn test_md060_complex_regex_with_escaped_pipes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content =
"| Challenge | Solution |\n|---|---|\n| Hour:minute:second | `^([0-1]?\\d\\|2[0-3]):[0-5]\\d:[0-5]\\d$` |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains(r"`^([0-1]?\d\|2[0-3]):[0-5]\d:[0-5]\d$`"),
"Complex regex with escaped pipes should be preserved"
);
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
}
#[test]
fn test_md060_compact_style() {
let rule = MD060TableFormat::new(true, "compact".to_string());
let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = "| Name | Age |\n| --- | --- |\n| Alice | 30 |";
assert_eq!(fixed, expected);
let lines: Vec<&str> = fixed.lines().collect();
assert!(lines[0].len() < 20, "Compact style should be short");
}
#[test]
fn test_md060_max_width_fallback() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| VeryLongColumnName | AnotherLongColumn | ThirdColumn |\n|---|---|---|\n| Data | Data | Data |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.lines().all(|line| line.len() <= 80),
"Wide tables should fall back to compact mode"
);
}
#[test]
fn test_md060_empty_cells() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| A | B | C |\n|---|---|---|\n| | X | |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains('|'), "Table structure should be preserved");
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 3, "All rows should be present");
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
}
#[test]
fn test_md060_mixed_content() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Age | City | Status |\n|---|---|---|---|\n| 中文 | 30 | NYC | ✅ |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("中文"), "CJK should be preserved");
assert!(fixed.contains("NYC"), "ASCII should be preserved");
assert!(fixed.contains("✅"), "Emoji should be preserved");
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].width(), lines[1].width(), "Display widths should match");
assert_eq!(lines[1].width(), lines[2].width(), "Display widths should match");
}
#[test]
fn test_md060_preserve_alignment_indicators() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = "| Left | Center | Right |\n| :--- | :----: | ----: |\n| A | B | C |";
assert_eq!(fixed, expected);
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
assert!(lines[1].contains(" :--- "), "Left alignment should have spaces");
assert!(lines[1].contains(" :----: "), "Center alignment should have spaces");
assert!(lines[1].contains(" ----: "), "Right alignment should have spaces");
}
#[test]
fn test_md060_table_with_trailing_newline() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.ends_with('\n'), "Trailing newline should be preserved");
}
#[test]
fn test_md060_multiple_tables() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "# First Table\n\n| A | B |\n|---|---|\n| 1 | 2222 |\n\n# Second Table\n\n| X | Y | Z |\n|---|---|---|\n| aaaa | b | c |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("# First Table"));
assert!(fixed.contains("# Second Table"));
let warnings = rule.check(&ctx).unwrap();
assert!(warnings.len() >= 6, "Should warn about both tables");
}
#[test]
fn test_md060_table_without_content_rows() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Header 1 | Header 2 |\n|---|---|";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("Header 1"));
assert!(fixed.contains("Header 2"));
}
#[test]
fn test_md060_none_style() {
let rule = MD060TableFormat::new(true, "none".to_string());
let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 0, "None style should not produce warnings");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "None style should not modify content");
}
#[test]
fn test_md060_single_column_table() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Column |\n|---|\n| Value1 |\n| Value2 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("Column"));
assert!(fixed.contains("Value1"));
assert!(fixed.contains("Value2"));
}
#[test]
fn test_md060_table_in_context() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content =
"# Documentation\n\nSome text before.\n\n| Name | Age |\n|---|---|\n| Alice | 30 |\n\nSome text after.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("# Documentation"));
assert!(fixed.contains("Some text before."));
assert!(fixed.contains("Some text after."));
assert!(fixed.contains("| Name | Age |"));
let lines: Vec<&str> = fixed.lines().collect();
let table_lines: Vec<&str> = lines
.iter()
.skip_while(|line| !line.starts_with('|'))
.take_while(|line| line.starts_with('|'))
.copied()
.collect();
assert_eq!(table_lines[0].len(), table_lines[1].len());
assert_eq!(table_lines[1].len(), table_lines[2].len());
}
#[test]
fn test_md060_warning_messages() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 3);
for warning in &warnings {
assert_eq!(warning.message, "Table columns should be aligned");
assert_eq!(warning.rule_name, Some("MD060".to_string()));
assert!(warning.fix.is_some(), "Each warning should have a fix");
}
}
#[test]
fn test_md060_escaped_pipes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Pattern | Description |\n|---|---|\n| `a\\|b` | Or operator |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("`a\\|b`"), "Escaped pipes should be preserved");
}
#[test]
fn test_md060_very_long_content() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let long_text = "A".repeat(100);
let content = format!("| Col1 | Col2 |\n|---|---|\n| {long_text} | B |");
let ctx = LintContext::new(&content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains(&long_text), "Long content should be preserved");
}
#[test]
fn test_md060_minimum_column_width() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| ID | First Name | Last Name | Department |\n|-|-|-|-|\n| 1 | John | Doe | Engineering |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(
lines[0].len(),
lines[1].len(),
"Header and delimiter should be same length"
);
assert_eq!(
lines[1].len(),
lines[2].len(),
"Delimiter and content should be same length"
);
assert!(
lines[0].contains("ID "),
"Short header 'ID' should be padded to minimum width"
);
assert!(lines[1].contains("---"), "Delimiter should have at least 3 dashes");
assert!(
lines[2].contains("1 "),
"Short content '1' should be padded to minimum width"
);
assert!(
lines[0].starts_with("| ID "),
"First column should be properly aligned with minimum width 3"
);
}
#[test]
fn test_md060_minimum_width_with_alignment_indicators() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| A | B | C |\n|:---|---:|:---:|\n| X | Y | Z |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
assert!(lines[1].contains(":---"), "Left alignment should be preserved");
assert!(lines[1].contains("---:"), "Right alignment should be preserved");
assert!(lines[1].contains(":---:"), "Center alignment should be preserved");
}
#[test]
fn test_md060_empty_header_table() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "|||\n|-|-|\n|lorem|ipsum|";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = "| | |\n| ----- | ----- |\n| lorem | ipsum |";
assert_eq!(fixed, expected, "Empty header table should be formatted");
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
}
#[test]
fn test_md060_delimiter_width_does_not_affect_alignment() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "|lorem|ipsum|\n|--------------|-|\n|dolor|sit|";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = "| lorem | ipsum |\n| ----- | ----- |\n| dolor | sit |";
assert_eq!(
fixed, expected,
"Delimiter row width should not affect column alignment"
);
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
}
#[test]
fn test_md060_content_alignment_left() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Left |\n|:-----|\n| A |\n| BB |\n| CCC |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
assert_eq!(lines[2].len(), lines[3].len());
assert_eq!(lines[3].len(), lines[4].len());
assert!(
lines[2].contains("| A |"),
"Single char should be left-aligned with padding on right"
);
assert!(
lines[3].contains("| BB |"),
"Two chars should be left-aligned with padding on right"
);
assert!(
lines[4].contains("| CCC |"),
"Three chars should be left-aligned with padding on right"
);
}
#[test]
fn test_md060_content_alignment_center() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Center |\n|:------:|\n| A |\n| BB |\n| CCC |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
assert_eq!(lines[2].len(), lines[3].len());
assert_eq!(lines[3].len(), lines[4].len());
assert!(
lines[2].contains("| A |"),
"Single char should be center-aligned, got: {}",
lines[2]
);
assert!(
lines[3].contains("| BB |"),
"Two chars should be center-aligned, got: {}",
lines[3]
);
assert!(
lines[4].contains("| CCC |"),
"Three chars should be center-aligned, got: {}",
lines[4]
);
}
#[test]
fn test_md060_content_alignment_right() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Right |\n|------:|\n| A |\n| BB |\n| CCC |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
assert_eq!(lines[2].len(), lines[3].len());
assert_eq!(lines[3].len(), lines[4].len());
assert!(
lines[2].contains("| A |"),
"Single char should be right-aligned with padding on left, got: {}",
lines[2]
);
assert!(
lines[3].contains("| BB |"),
"Two chars should be right-aligned with padding on left, got: {}",
lines[3]
);
assert!(
lines[4].contains("| CCC |"),
"Three chars should be right-aligned with padding on left, got: {}",
lines[4]
);
}
#[test]
fn test_md060_mixed_column_alignments() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |\n| AA | BB | CC |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
assert_eq!(lines[2].len(), lines[3].len());
let row1 = lines[2];
let row2 = lines[3];
assert!(
row1.starts_with("| A "),
"First column should be left-aligned in row 1, got: {row1}",
);
assert!(
row2.starts_with("| AA"),
"First column should be left-aligned in row 2, got: {row2}",
);
assert!(
row1.contains("| C |"),
"Third column should be right-aligned in row 1, got: {row1}",
);
assert!(
row1.ends_with("| C |"),
"Third column should be at end of row 1, got: {row1}",
);
assert!(
row2.contains("| CC |"),
"Third column should be right-aligned in row 2, got: {row2}",
);
assert!(
row2.ends_with("| CC |"),
"Third column should be at end of row 2, got: {row2}",
);
}
#[test]
fn test_md060_tables_in_html_comments_should_not_be_formatted() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "# Normal table\n\n| A | B |\n|---|---|\n| C | D |\n\n<!-- Commented table\n| X | Y |\n|---|---|\n| Z | W |\n-->\n\n| E | F |\n|---|---|\n| G | H |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
let non_comment_warnings: Vec<_> = warnings
.iter()
.filter(|w| {
let line = w.line;
(3..=5).contains(&line) || (13..=15).contains(&line)
})
.collect();
assert_eq!(
non_comment_warnings.len(),
warnings.len(),
"Should only warn about tables outside HTML comments. Got {} warnings total, expected 6",
warnings.len()
);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("| X | Y |"), "Commented table should not be modified");
assert!(fixed.contains("| Z | W |"), "Commented table should not be modified");
assert!(
fixed.contains("| A | B |") || fixed.contains("| A | B |"),
"Normal table should be formatted"
);
assert!(
fixed.contains("| E | F |") || fixed.contains("| E | F |"),
"Normal table should be formatted"
);
}
#[test]
fn test_md060_zero_width_characters() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Status |\n|---|---|\n| Test\u{200B}Word | Active\u{200C}User |\n| Word\u{2060}Join | OK |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(
warnings.len(),
0,
"Tables with zero-width characters should be skipped (no warnings)"
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed, content,
"Tables with zero-width characters should not be modified"
);
assert!(
fixed.contains("Test\u{200B}Word"),
"Zero Width Space should be preserved"
);
assert!(
fixed.contains("Active\u{200C}User"),
"Zero Width Non-Joiner should be preserved"
);
assert!(fixed.contains("Word\u{2060}Join"), "Word Joiner should be preserved");
}
#[test]
fn test_md060_rtl_text_arabic() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | City |\n|---|---|\n| أحمد | القاهرة |\n| محمد | دبي |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("أحمد"), "Arabic name should be preserved");
assert!(fixed.contains("القاهرة"), "Arabic city should be preserved");
assert!(fixed.contains("محمد"), "Arabic name should be preserved");
assert!(fixed.contains("دبي"), "Arabic city should be preserved");
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(
lines[0].width(),
lines[1].width(),
"Display widths should match for RTL text"
);
assert_eq!(
lines[1].width(),
lines[2].width(),
"Display widths should match for RTL text"
);
assert_eq!(
lines[2].width(),
lines[3].width(),
"Display widths should match for RTL text"
);
}
#[test]
fn test_md060_rtl_text_hebrew() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| שם | עיר |\n|---|---|\n| דוד | תל אביב |\n| שרה | ירושלים |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("דוד"), "Hebrew name should be preserved");
assert!(fixed.contains("תל אביב"), "Hebrew city should be preserved");
assert!(fixed.contains("שרה"), "Hebrew name should be preserved");
assert!(fixed.contains("ירושלים"), "Hebrew city should be preserved");
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(
lines[0].width(),
lines[1].width(),
"Display widths should match for RTL text"
);
assert_eq!(
lines[1].width(),
lines[2].width(),
"Display widths should match for RTL text"
);
assert_eq!(
lines[2].width(),
lines[3].width(),
"Display widths should match for RTL text"
);
}
#[test]
fn test_md060_mismatched_column_counts_more_in_header() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| A | B | C | D |\n|---|---|---|\n| X | Y |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "Should handle mismatched column counts gracefully");
let fixed = result.unwrap();
assert!(
fixed.contains('A') || fixed.contains('X'),
"Content should be preserved"
);
}
#[test]
fn test_md060_mismatched_column_counts_more_in_content() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| A | B |\n|---|---|\n| X | Y | Z | W |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "Should handle mismatched column counts gracefully");
let fixed = result.unwrap();
assert!(
fixed.contains('A') || fixed.contains('X'),
"Content should be preserved"
);
}
#[test]
fn test_md060_escaped_pipes_outside_code() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Operator | Example |\n|---|---|\n| OR | a \\| b |\n| Pipe | x \\| y \\| z |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("\\|"), "Escaped pipes should be preserved");
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 4, "All rows should be present");
assert!(
lines[2].contains("a \\| b"),
"First escaped pipe example should be in single cell, got: {}",
lines[2]
);
assert!(
lines[3].contains("x \\| y \\| z"),
"Second escaped pipe example should be in single cell, got: {}",
lines[3]
);
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
assert_eq!(lines[2].len(), lines[3].len());
}
#[test]
fn test_md060_combining_characters_diacritics() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| City | Country |\n|---|---|\n| café | français |\n| São Paulo | Brasil |\n| Zürich | Schweiz |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("café"), "Café with combining acute should be preserved");
assert!(fixed.contains("São"), "São with combining tilde should be preserved");
assert!(
fixed.contains("Zürich"),
"Zürich with combining umlaut should be preserved"
);
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(
lines[0].width(),
lines[1].width(),
"Display widths should match with diacritics"
);
assert_eq!(
lines[1].width(),
lines[2].width(),
"Display widths should match with diacritics"
);
}
#[test]
fn test_md060_skin_tone_modifiers() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| User | Avatar |\n|---|---|\n| Alice | 👍🏻 |\n| Bob | 👋🏿 |\n| Carol | 🤝🏽 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("👍🏻"), "Emoji with light skin tone should be preserved");
assert!(fixed.contains("👋🏿"), "Emoji with dark skin tone should be preserved");
assert!(fixed.contains("🤝🏽"), "Emoji with medium skin tone should be preserved");
}
#[test]
fn test_md060_flag_emojis() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Country | Flag |\n|---|---|\n| USA | 🇺🇸 |\n| Japan | 🇯🇵 |\n| France | 🇫🇷 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("🇺🇸"), "US flag should be preserved");
assert!(fixed.contains("🇯🇵"), "Japan flag should be preserved");
assert!(fixed.contains("🇫🇷"), "France flag should be preserved");
}
#[test]
fn test_md060_tables_in_blockquotes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "> | Name | Age |\n> |---|---|\n> | Alice | 30 |\n\nNormal text\n\n| X | Y |\n|---|---|\n| A | B |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.starts_with("> "), "Blockquote markers should be preserved");
assert!(
fixed.contains("Alice") || fixed.contains("Name"),
"Table in blockquote should be present"
);
assert!(
fixed.contains('A') && fixed.contains('B'),
"Normal table should be present"
);
}
#[test]
fn test_md060_tables_in_nested_blockquotes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = ">> | Col1 | Col2 |\n>> |---|---|\n>> | A | B |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert!(lines[0].starts_with(">> "), "Header should preserve >> prefix");
assert!(lines[1].starts_with(">> "), "Delimiter should preserve >> prefix");
assert!(lines[2].starts_with(">> "), "Content should preserve >> prefix");
}
#[test]
fn test_md060_tables_in_deeply_nested_blockquotes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = ">>> | X | Y | Z |\n>>> |---|---|---|\n>>> | 1 | 2 | 3 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
for (i, line) in lines.iter().enumerate() {
assert!(
line.starts_with(">>> "),
"Line {i} should preserve >>> prefix, got: {line}"
);
}
}
#[test]
fn test_md060_blockquote_table_all_styles() {
let content = "> | A | B |\n> |---|---|\n> | 1 | 2 |";
for style in ["aligned", "compact", "tight"] {
let rule = MD060TableFormat::new(true, style.to_string());
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
for (i, line) in lines.iter().enumerate() {
assert!(
line.starts_with("> "),
"Style '{style}' line {i} should preserve '> ' prefix, got: {line}"
);
}
}
}
#[test]
fn test_md060_blockquote_table_compact_prefix() {
let rule = MD060TableFormat::new(true, "compact".to_string());
let content = ">| A | B |\n>|---|---|\n>| 1 | 2 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
for (i, line) in lines.iter().enumerate() {
assert!(line.starts_with('>'), "Line {i} should start with >, got: {line}");
}
}
#[test]
fn test_md060_blockquote_table_preserves_alignment() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "> | Left | Center | Right |\n> |:---|:---:|---:|\n> | A | B | C |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.starts_with("> "), "Should start with blockquote prefix");
assert!(fixed.contains(":---"), "Left alignment should be preserved");
assert!(fixed.contains("---:"), "Right alignment should be preserved");
}
#[test]
fn test_md060_multiple_blockquote_tables() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "> | A | B |\n> |---|---|\n> | 1 | 2 |\n>\n> | X | Y |\n> |---|---|\n> | 3 | 4 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
for (i, line) in lines.iter().enumerate() {
if !line.is_empty() {
assert!(
line.starts_with('>'),
"Line {i} should have blockquote prefix, got: {line}"
);
}
}
assert!(
fixed.contains('1') && fixed.contains('2'),
"First table content preserved"
);
assert!(
fixed.contains('3') && fixed.contains('4'),
"Second table content preserved"
);
}
#[test]
fn test_md060_adjacent_tables_without_blank_line() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| A | B |\n|---|---|\n| 1 | 2 |\n| C | D |\n|---|---|\n| 3 | 4 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "Adjacent tables should not cause crash");
let fixed = result.unwrap();
assert!(
fixed.contains('1') && fixed.contains('2'),
"First table content should be preserved"
);
assert!(
fixed.contains('3') && fixed.contains('4'),
"Second table content should be preserved"
);
}
#[test]
fn test_md060_maximum_column_count_stress() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let columns = 100;
let header_row = format!(
"| {} |",
(0..columns).map(|i| format!("C{i}")).collect::<Vec<_>>().join(" | ")
);
let delimiter_row = format!("| {} |", vec!["---"; columns].join(" | "));
let content_row = format!(
"| {} |",
(0..columns).map(|i| i.to_string()).collect::<Vec<_>>().join(" | ")
);
let content = format!("{header_row}\n{delimiter_row}\n{content_row}");
let ctx = LintContext::new(&content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "Should handle 100 columns without crashing");
let fixed = result.unwrap();
assert!(fixed.contains("C0"), "First column should be present");
assert!(fixed.contains("C99"), "Last column should be present");
}
#[test]
fn test_md060_fix_idempotency() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | NYC |\n| Bob | 25 | LA |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed_once = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed_once, MarkdownFlavor::Standard, None);
let fixed_twice = rule.fix(&ctx2).unwrap();
assert_eq!(
fixed_once, fixed_twice,
"Applying fix twice should produce the same result as applying it once (idempotency)"
);
let warnings = rule.check(&ctx2).unwrap();
assert_eq!(warnings.len(), 0, "Already-formatted table should produce no warnings");
}
#[test]
fn test_md060_completely_empty_table() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| | | |\n|---|---|---|\n| | | |\n| | | |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "Empty table should not crash");
let fixed = result.unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 4);
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
assert_eq!(lines[2].len(), lines[3].len());
}
#[test]
fn test_md060_table_with_no_delimiter() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Age |\n| Alice | 30 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "Missing delimiter should not crash");
}
#[test]
fn test_md060_single_row_table_header_only() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Column A | Column B | Column C |\n|---|---|---|";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(
lines[0].len(),
lines[1].len(),
"Header and delimiter should have equal length"
);
assert!(fixed.contains("Column A"));
assert!(fixed.contains("Column B"));
assert!(fixed.contains("Column C"));
}
#[test]
fn test_md060_varying_column_counts_per_row() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| A | B | C | D |\n|---|---|\n| X |\n| Y | Z | W | V | U |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "Varying column counts should not crash");
}
#[test]
fn test_md060_delimiter_with_no_dashes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| A | B |\n|:::|:::|\n| X | Y |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "Invalid delimiter should not crash");
}
#[test]
fn test_md060_bidirectional_text_mixed_ltr_rtl() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| English | العربية |\n|---|---|\n| Hello | مرحبا |\n| World | عالم |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("English"));
assert!(fixed.contains("العربية"));
assert!(fixed.contains("Hello"));
assert!(fixed.contains("مرحبا"));
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].width(), lines[1].width());
assert_eq!(lines[1].width(), lines[2].width());
assert_eq!(lines[2].width(), lines[3].width());
}
#[test]
fn test_md060_unicode_variation_selectors() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Char | Style |\n|---|---|\n| ☺︎ | Text |\n| ☺️ | Emoji |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("Text"));
assert!(fixed.contains("Emoji"));
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 4);
}
#[test]
fn test_md060_unicode_control_characters() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Value |\n|---|---|\n| Test\u{0001} | Data |\n| Item | Info\u{001F} |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "Control characters should not crash formatting");
}
#[test]
fn test_md060_unicode_normalization_issues() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| NFC | NFD |\n|---|---|\n| café | cafe\u{0301} |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("café"));
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 3);
}
#[test]
fn test_md060_mixed_emoji_types() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Type | Example |\n|---|---|\n| Basic | 😀 |\n| Gender | 👨 |\n| Number | #️⃣ |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("😀"));
assert!(fixed.contains("👨"));
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 5);
}
#[test]
fn test_md060_extremely_wide_single_cell() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let long_text = "A".repeat(10000);
let content = format!("| Short | Long |\n|---|---|\n| X | {long_text} |");
let ctx = LintContext::new(&content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "Extremely wide cell should not crash");
let fixed = result.unwrap();
assert!(fixed.contains(&long_text), "Long text should be preserved");
}
#[test]
fn test_md060_many_rows_stress() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let mut lines = vec!["| ID | Name | Value |".to_string(), "|---|---|---|".to_string()];
for i in 0..1000 {
lines.push(format!("| {i} | Row{i} | Data{i} |"));
}
let content = lines.join("\n");
let ctx = LintContext::new(&content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "1000 rows should not crash");
let fixed = result.unwrap();
assert!(fixed.contains("Row0"));
assert!(fixed.contains("Row999"));
}
#[test]
fn test_md060_deeply_nested_inline_code() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Code | Description |\n|---|---|\n| `a\\|b` | Simple |\n| `x\\|y\\|z` | Multiple |\n| `{a\\|b}\\|{c\\|d}` | Complex |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("`a\\|b`"));
assert!(fixed.contains("`x\\|y\\|z`"));
assert!(fixed.contains("`{a\\|b}\\|{c\\|d}`"));
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
}
#[test]
fn test_md060_table_with_links() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Link |\n|---|---|\n| GitHub | [Link](https://github.com) |\n| Google | [Search](https://google.com) |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("[Link](https://github.com)"));
assert!(fixed.contains("[Search](https://google.com)"));
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
}
#[test]
fn test_md060_table_with_html_entities() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Symbol | HTML |\n|---|---|\n| Less than | < |\n| Greater | > |\n| Ampersand | & |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("<"));
assert!(fixed.contains(">"));
assert!(fixed.contains("&"));
}
#[test]
fn test_md060_table_with_bold_and_italic() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content =
"| Text | Style |\n|---|---|\n| **Bold** | Strong |\n| *Italic* | Emphasis |\n| ***Both*** | Combined |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("**Bold**"));
assert!(fixed.contains("*Italic*"));
assert!(fixed.contains("***Both***"));
}
#[test]
fn test_md060_table_with_strikethrough() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Status | Item |\n|---|---|\n| Done | ~~Old~~ |\n| Active | Current |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("~~Old~~"));
}
#[test]
fn test_md060_cells_with_leading_trailing_spaces() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Value |\n|---|---|\n| Spaced | Data |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
}
#[test]
fn test_md060_cells_with_tabs() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Value |\n|---|---|\n| Tab\there | Data |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("Tab\there"));
}
#[test]
fn test_md060_cells_with_newline_escape() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Pattern | Example |\n|---|---|\n| Newline | Line\\nBreak |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("Line\\nBreak"));
}
#[test]
fn test_md060_delimiter_with_many_dashes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| A | B | C |\n|----------|---|---------------------------|\n| X | Y | Z |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
}
#[test]
fn test_md060_all_alignment_combinations() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content =
"| Default | Left | Right | Center |\n|---|:---|---:|:---:|\n| A | B | C | D |\n| AA | BB | CC | DD |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
let delimiter_row = lines[1];
assert!(
delimiter_row.contains("---") || delimiter_row.contains("----"),
"Default alignment should have dashes"
);
assert!(
delimiter_row.contains(":---") || delimiter_row.contains(":----"),
"Left alignment should have colon-dashes"
);
assert!(
delimiter_row.contains("---:") || delimiter_row.contains("----:"),
"Right alignment should have dashes-colon"
);
assert!(
delimiter_row.chars().filter(|&c| c == ':').count() >= 4,
"Should have at least 4 colons (2 for center, 1 for left, 1 for right)"
);
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
assert_eq!(lines[2].len(), lines[3].len());
}
#[test]
fn test_md060_unicode_in_aligned_columns() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | 中 | 1 |\n| AAA | 中中中 | 111 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].width(), lines[1].width());
assert_eq!(lines[1].width(), lines[2].width());
assert_eq!(lines[2].width(), lines[3].width());
assert!(fixed.contains("中"));
assert!(fixed.contains("中中中"));
}
#[test]
fn test_md060_empty_and_whitespace_only_cells_mixed() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| A | B | C |\n|---|---|---|\n| | | X |\n| Y | | |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
assert_eq!(lines[2].len(), lines[3].len());
assert!(fixed.contains('X'));
assert!(fixed.contains('Y'));
}
#[test]
fn test_md060_issue_164_already_aligned_short_separators() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| a | b | c |\n| :-- | :-: | --: |\n| 1 | 2 | 3 |\n| 10 | 20 | 30 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(
warnings.len(),
0,
"Already-aligned table with short (3-char) separators should not produce warnings"
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed, content,
"Already-aligned table should be preserved exactly as-is"
);
let lines: Vec<&str> = fixed.lines().collect();
let first_len = lines[0].len();
assert!(
lines.iter().all(|line| line.len() == first_len),
"All rows should maintain consistent length"
);
assert!(
fixed.contains("| :-- | :-: | --: |"),
"Short separator format should be preserved"
);
}
#[test]
fn test_md060_issue_164_misaligned_short_separators_detected() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| a | b | c |\n| :-- | :-: | --: |\n| 1 | 2 | 3 |\n| 10 | 20 | 30 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
!warnings.is_empty(),
"Misaligned table should produce warnings even with short separators"
);
}
#[test]
fn test_md060_mkdocs_flavor_pipes_in_code_spans_issue_165() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Type | Example |\n| - | - |\n| Union | `x | y` |\n| Dict | `dict` |";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("`x | y`"),
"Inline code with pipe should be preserved as single cell content, got: {fixed}"
);
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 4, "Should have 4 lines");
assert_eq!(
lines[0].len(),
lines[1].len(),
"Header and delimiter should match: '{}' vs '{}'",
lines[0],
lines[1]
);
assert_eq!(
lines[1].len(),
lines[2].len(),
"Delimiter and content should match: '{}' vs '{}'",
lines[1],
lines[2]
);
assert_eq!(
lines[2].len(),
lines[3].len(),
"Content rows should match: '{}' vs '{}'",
lines[2],
lines[3]
);
}
#[test]
fn test_md060_mkdocs_flavor_various_code_spans_with_pipes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content =
"| Type | Syntax |\n| - | - |\n| Union | `A | B` |\n| Optional | `T | None` |\n| Multiple | `a | b | c` |";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("`A | B`"), "Union type should be preserved");
assert!(fixed.contains("`T | None`"), "Optional type should be preserved");
assert!(fixed.contains("`a | b | c`"), "Multiple pipes should be preserved");
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 5, "Should have 5 lines");
for i in 0..lines.len() - 1 {
assert_eq!(
lines[i].len(),
lines[i + 1].len(),
"Lines {} and {} should have same length",
i,
i + 1
);
}
}
#[test]
fn test_md060_mkdocs_flavor_no_false_positives() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Type | Example |\n| ----- | -------- |\n| Union | `x | y` |";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"Should have no warnings for aligned 2-column table with MkDocs flavor, got: {:?}",
warnings.iter().map(|w| &w.message).collect::<Vec<_>>()
);
}
#[test]
fn test_md060_mkdocs_flavor_fix_preserves_inline_code_pipes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Type | Example |\n|-|-|\n| Union | `x | y` |\n| Dict | `dict` |";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("`x | y`"),
"Inline code content should be preserved intact, got: {fixed}"
);
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 4, "Table should have 4 lines");
let union_row = lines[2];
assert!(
union_row.contains("`x | y`"),
"Union row should contain intact inline code, got: {union_row}"
);
}
#[test]
fn test_md060_mkdocs_flavor_compact_style() {
let rule = MD060TableFormat::new(true, "compact".to_string());
let content = "| Type | Example |\n|-|-|\n| Union | `x | y` |";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("`x | y`"),
"Inline code should be preserved in compact mode"
);
assert!(fixed.contains("| Type | Example |") || fixed.contains("| Type | Example |"));
}
#[test]
fn test_md060_mkdocs_flavor_tight_style() {
let rule = MD060TableFormat::new(true, "tight".to_string());
let content = "| Type | Example |\n|-|-|\n| Union | `x | y` |";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("`x | y`"),
"Inline code should be preserved in tight mode"
);
assert!(fixed.contains("|Type|"), "Should have tight formatting");
}
#[test]
fn test_md060_standard_flavor_pipes_in_code_are_delimiters() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Type | Example |\n|-|-|\n| Union | `x | y` |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let _lines: Vec<&str> = fixed.lines().collect();
}
#[test]
fn test_md060_mkdocs_flavor_escaped_and_inline_code_pipes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Type | Example |\n|-|-|\n| Escaped | a \\| b |\n| Code | `x | y` |";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("a \\| b"), "Escaped pipe should be preserved");
assert!(fixed.contains("`x | y`"), "Inline code pipe should be preserved");
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 4, "Should have 4 lines");
}
fn default_md013_config() -> MD013Config {
MD013Config::default()
}
#[test]
fn test_md060_loose_last_column_basic() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Name | Description |\n|---|---|\n| Foo | Short |\n| Bar | A much longer description |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(
lines[0].len(),
lines[1].len(),
"Header and delimiter should be same length"
);
assert_eq!(
lines[2].len(),
lines[0].len(),
"Body row with short content should be padded to header width"
);
assert!(
lines[3].len() > lines[0].len(),
"Body row with long content ({} chars) should extend beyond header ({} chars)",
lines[3].len(),
lines[0].len()
);
}
#[test]
fn test_md060_loose_last_column_disabled_by_default() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Description |\n|---|---|\n| Foo | Short |\n| Bar | Longer text |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len(), "Header and delimiter should match");
assert_eq!(
lines[1].len(),
lines[2].len(),
"Delimiter and first body row should match"
);
assert_eq!(lines[2].len(), lines[3].len(), "Body rows should match");
}
#[test]
fn test_md060_loose_last_column_header_delimiter_still_aligned() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| ID | Name | Description |\n|---|---|---|\n| 1 | A | X |\n| 2 | B | Y |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(
lines[0].len(),
lines[1].len(),
"Header and delimiter should be same length even with loose-last-column"
);
}
#[test]
fn test_md060_loose_last_column_multiple_columns() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| A | B | C | D |\n|---|---|---|---|\n| 1 | 2 | 3 | Short |\n| 1 | 2 | 3 | Longer text here |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 4, "Should have 4 lines");
assert!(fixed.contains("| A "), "Header A should be in output");
assert!(fixed.contains("| B "), "Header B should be in output");
assert!(fixed.contains("| C "), "Header C should be in output");
assert!(fixed.contains("| D "), "Header D should be in output");
assert!(fixed.contains("Short"), "Short text should be in output");
assert!(fixed.contains("Longer text here"), "Long text should be in output");
}
#[test]
fn test_md060_loose_last_column_single_column_table() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Description |\n|---|\n| Short |\n| A much longer description |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_ok(), "Single column with loose-last-column should not crash");
let fixed = result.unwrap();
assert!(fixed.contains("Short"), "Short text should be preserved");
assert!(
fixed.contains("A much longer description"),
"Long text should be preserved"
);
}
#[test]
fn test_md060_column_align_header_basic() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Left, column_align_header: Some(ColumnAlign::Center), column_align_body: None,
loose_last_column: false,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| A | B |\n|---|---|\n| Long | Text |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 3, "Should have 3 lines");
let header = lines[0];
let body = lines[2];
assert!(
header.contains("| ") || header.contains("| A "),
"Header should show centering pattern, got: {header}"
);
assert!(
body.starts_with("| Long"),
"Body should be left-aligned (content right after pipe), got: {body}"
);
}
#[test]
fn test_md060_column_align_body_basic() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Left, column_align_header: None,
column_align_body: Some(ColumnAlign::Right), loose_last_column: false,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Long | Text |\n|---|---|\n| A | B |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 3, "Should have 3 lines");
let header = lines[0];
let body = lines[2];
assert!(
header.starts_with("| Long"),
"Header should be left-aligned, got: {header}"
);
assert!(
body.contains(" A |") || body.contains(" A |"),
"Body should be right-aligned with padding before 'A', got: {body}"
);
}
#[test]
fn test_md060_column_align_header_and_body_different() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto, column_align_header: Some(ColumnAlign::Center), column_align_body: Some(ColumnAlign::Left), loose_last_column: false,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| ColumnA | ColumnB |\n|---|---|\n| X | Y |\n| XX | YY |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 4, "Should have 4 lines");
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
assert_eq!(lines[2].len(), lines[3].len());
}
#[test]
fn test_md060_column_align_header_only_set() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Right, column_align_header: Some(ColumnAlign::Left), column_align_body: None, loose_last_column: false,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| A | B |\n|---|---|\n| X | Y |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
}
#[test]
fn test_md060_column_align_body_only_set() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Left, column_align_header: None, column_align_body: Some(ColumnAlign::Center), loose_last_column: false,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Name | Value |\n|---|---|\n| Key | 42 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0].len(), lines[1].len());
assert_eq!(lines[1].len(), lines[2].len());
}
#[test]
fn test_md060_column_align_auto_with_header_body_override() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto, column_align_header: Some(ColumnAlign::Center), column_align_body: None, loose_last_column: false,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains(":---"), "Left alignment marker should be preserved");
assert!(fixed.contains("---:"), "Right alignment marker should be preserved");
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 3);
}
#[test]
fn test_md060_column_align_all_combinations() {
for header_align in [
Some(ColumnAlign::Left),
Some(ColumnAlign::Center),
Some(ColumnAlign::Right),
None,
] {
for body_align in [
Some(ColumnAlign::Left),
Some(ColumnAlign::Center),
Some(ColumnAlign::Right),
None,
] {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: header_align,
column_align_body: body_align,
loose_last_column: false,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| A | B |\n|---|---|\n| X | Y |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(
result.is_ok(),
"Should not panic for header={header_align:?}, body={body_align:?}"
);
}
}
}
#[test]
fn test_md060_loose_last_column_with_header_body_alignment() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: Some(ColumnAlign::Center),
column_align_body: Some(ColumnAlign::Left),
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Name | Description |\n|---|---|\n| A | Short |\n| B | A very long description |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len(), "Header and delimiter should match");
assert!(fixed.contains("Short"), "Short text preserved");
assert!(fixed.contains("A very long description"), "Long text preserved");
}
#[test]
fn test_md060_features_idempotency() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: Some(ColumnAlign::Center),
column_align_body: Some(ColumnAlign::Left),
loose_last_column: false, aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config.clone(), default_md013_config(), false);
let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | NYC |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed_once = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed_once, MarkdownFlavor::Standard, None);
let rule2 = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let fixed_twice = rule2.fix(&ctx2).unwrap();
assert_eq!(fixed_once, fixed_twice, "Applying fix twice should produce same result");
}
#[test]
fn test_md060_loose_last_column_exact_output() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| A | B |\n|---|---|\n| X | Short |\n| Y | Much longer text |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = "| A | B |\n| --- | --- |\n| X | Short |\n| Y | Much longer text |";
assert_eq!(
fixed, expected,
"Loose last column should cap last column width at header text width"
);
}
#[test]
fn test_md060_loose_last_column_empty_cell() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| A | Description |\n|---|---|\n| X | |\n| Y | Has content |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(
lines[2].len(),
lines[0].len(),
"Row with empty last cell should be padded to header width"
);
assert_eq!(
lines[3].len(),
lines[0].len(),
"Row with content matching header width should equal header length"
);
}
#[test]
fn test_md060_loose_last_column_preserves_alignment_markers() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Left | Right |\n|:---|---:|\n| A | B |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains(":---"), "Left alignment marker should be preserved");
assert!(
fixed.contains("---:") || fixed.contains("-:"),
"Right alignment marker should be preserved"
);
}
#[test]
fn test_md060_column_align_header_center_exact() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Left,
column_align_header: Some(ColumnAlign::Center),
column_align_body: None, loose_last_column: false,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| A |\n|---|\n| Long |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
let header = lines[0];
let a_pos = header.find('A').expect("A should be in header");
let pipe_after_a = header[a_pos..].find('|').expect("Pipe should follow A");
let first_pipe = header.find('|').unwrap();
let chars_before_a = a_pos - first_pipe - 1;
let chars_after_a = pipe_after_a - 1;
assert!(
chars_before_a > 0,
"Centered header should have space before 'A', got {chars_before_a} chars before"
);
assert!(
chars_after_a > 0,
"Centered header should have space after 'A', got {chars_after_a} chars after"
);
let body = lines[2];
assert!(
body.contains("| Long"),
"Body should be left-aligned with 'Long' right after pipe, got: {body}"
);
}
#[test]
fn test_md060_column_align_body_right_exact() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Left,
column_align_header: None, column_align_body: Some(ColumnAlign::Right),
loose_last_column: false,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Long |\n|---|\n| X |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
let header = lines[0];
assert!(
header.contains("| Long"),
"Header should be left-aligned, got: {header}"
);
let body = lines[2];
let x_pos = body.find('X').expect("X should be in body");
let first_pipe = body.find('|').unwrap();
let chars_before_x = x_pos - first_pipe - 1;
assert!(
chars_before_x >= 3,
"Right-aligned body should have multiple spaces before 'X', got {chars_before_x} chars before. Line: {body}"
);
}
#[test]
fn test_md060_delimiter_unaffected_by_column_align() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Center,
column_align_header: Some(ColumnAlign::Right),
column_align_body: Some(ColumnAlign::Left),
loose_last_column: false,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| A | B |\n|---|---|\n| X | Y |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
let delimiter = lines[1];
assert!(
!delimiter.contains('A') && !delimiter.contains('B') && !delimiter.contains('X') && !delimiter.contains('Y'),
"Delimiter should not contain cell content, got: {delimiter}"
);
assert!(
delimiter.contains("---"),
"Delimiter should contain dashes, got: {delimiter}"
);
}
#[test]
fn test_md060_loose_last_column_with_cjk() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| A | Name |\n|---|---|\n| X | 中文 |\n| Y | English |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("中文"), "CJK content should be preserved");
assert!(fixed.contains("English"), "ASCII content should be preserved");
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 4, "Should have 4 lines");
}
#[test]
fn test_md060_unordered_list_continuation_table() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "- Test\n | c1 | c2 |\n |-|-|\n | foo | bar |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0], "- Test", "List item text unchanged");
assert!(
lines[1].starts_with(" "),
"Header line must preserve 2-space indentation, got: {:?}",
lines[1]
);
assert!(
lines[2].starts_with(" "),
"Delimiter line must preserve 2-space indentation, got: {:?}",
lines[2]
);
assert!(
lines[3].starts_with(" "),
"Data line must preserve 2-space indentation, got: {:?}",
lines[3]
);
assert!(lines[1].contains("c1"));
assert!(lines[1].contains("c2"));
assert!(lines[3].contains("foo"));
assert!(lines[3].contains("bar"));
}
#[test]
fn test_md060_ordered_list_continuation_table() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "1. Text\n | h1 | h2 |\n |---|---|\n | d1 | d2 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0], "1. Text");
assert!(
lines[1].starts_with(" "),
"Header must have 3-space indent for ordered list, got: {:?}",
lines[1]
);
assert!(
lines[2].starts_with(" "),
"Delimiter must have 3-space indent, got: {:?}",
lines[2]
);
assert!(
lines[3].starts_with(" "),
"Data row must have 3-space indent, got: {:?}",
lines[3]
);
}
#[test]
fn test_md060_nested_list_continuation_table() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "- Outer\n - Inner\n | h1 | h2 |\n |---|---|\n | d1 | d2 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0], "- Outer");
assert_eq!(lines[1], " - Inner");
assert!(
lines[2].starts_with(" "),
"Nested table header must have 4-space indent, got: {:?}",
lines[2]
);
assert!(
lines[3].starts_with(" "),
"Nested table delimiter must have 4-space indent, got: {:?}",
lines[3]
);
assert!(
lines[4].starts_with(" "),
"Nested table data must have 4-space indent, got: {:?}",
lines[4]
);
}
#[test]
fn test_md060_non_list_indented_table_no_list_context() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "Some text\n| h1 | h2 |\n|---|---|\n| d1 | d2 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0], "Some text");
assert!(
!lines[1].starts_with(' '),
"Non-list table should not get indentation, got: {:?}",
lines[1]
);
}
#[test]
fn test_md060_same_line_list_table_no_regression() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "- | h1 | h2 |\n |---|---|\n | d1 | d2 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert!(
lines[0].starts_with("- "),
"Same-line list table header must keep marker, got: {:?}",
lines[0]
);
assert!(
lines[1].starts_with(" "),
"Same-line list table delimiter must keep indent, got: {:?}",
lines[1]
);
assert!(
lines[2].starts_with(" "),
"Same-line list table data must keep indent, got: {:?}",
lines[2]
);
}
#[test]
fn test_md060_continuation_table_idempotency() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "- Test\n | c1 | c2 |\n |-|-|\n | foo | bar |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed1 = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed1, MarkdownFlavor::Standard, None);
let fixed2 = rule.fix(&ctx2).unwrap();
assert_eq!(
fixed1, fixed2,
"Fix must be idempotent:\n first: {fixed1:?}\n second: {fixed2:?}",
);
}
#[test]
fn test_md060_blank_line_between_text_and_continuation_table() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "- Text\n\n | h1 | h2 |\n |---|---|\n | d1 | d2 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert!(
lines[2].starts_with(" "),
"Table after blank line should keep indent, got: {:?}",
lines[2]
);
assert!(
lines[3].starts_with(" "),
"Delimiter after blank line should keep indent, got: {:?}",
lines[3]
);
assert!(
lines[4].starts_with(" "),
"Data after blank line should keep indent, got: {:?}",
lines[4]
);
}
#[test]
fn test_md060_nested_list_table_at_parent_level() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "- Parent\n - Child\n\n | h1 | h2 |\n |---|---|\n | d1 | d2 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0], "- Parent");
assert_eq!(lines[1], " - Child");
assert!(
lines[3].starts_with(" "),
"Table at parent level must keep 2-space indent, got: {:?}",
lines[3]
);
assert!(
lines[4].starts_with(" "),
"Delimiter at parent level must keep 2-space indent, got: {:?}",
lines[4]
);
assert!(
lines[5].starts_with(" "),
"Data at parent level must keep 2-space indent, got: {:?}",
lines[5]
);
}
#[test]
fn test_md060_deeply_indented_not_code_block() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "- Item\n | h1 | h2 |\n |---|---|\n | d1 | d2 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert!(
lines[1].starts_with(" "),
"Table should be normalized to content indent, got: {:?}",
lines[1]
);
}
#[test]
fn test_md060_code_block_boundary_not_treated_as_table() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "- Item\n | h1 | h2 |\n |---|---|\n | d1 | d2 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
let header_indent = lines[1].len() - lines[1].trim_start().len();
assert_ne!(
header_indent, 2,
"Code-block-depth content should not get list table treatment, got indent: {header_indent}",
);
}
#[test]
fn test_md060_mixed_ordered_unordered_nested_continuation() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "1. Ordered\n - Unordered\n\n | h1 | h2 |\n |---|---|\n | d1 | d2 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0], "1. Ordered");
assert!(
lines[3].starts_with(" "),
"Table at ordered list level must keep 3-space indent, got: {:?}",
lines[3]
);
}
#[test]
fn test_md060_atx_heading_with_pipe_not_misidentified_as_table() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "#### heading|with pipe\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"ATX heading with pipe should not trigger MD060, got {warnings:?}"
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "ATX heading with pipe should not be modified");
}
#[test]
fn test_md060_atx_heading_with_pipe_idempotent() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "#### ®aAA|ᯗ\n";
let ctx1 = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed1 = rule.fix(&ctx1).unwrap();
let ctx2 = LintContext::new(&fixed1, MarkdownFlavor::Standard, None);
let fixed2 = rule.fix(&ctx2).unwrap();
assert_eq!(fixed1, fixed2, "Fix must be idempotent for heading with unicode pipe");
}
#[test]
fn test_md060_heading_adjacent_to_table_not_absorbed() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "## Section|A\n\n| Col1 | Col2 |\n| ---- | ---- |\n| a | b |\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.starts_with("## Section|A\n"),
"Heading must not be reformatted, got: {fixed:?}"
);
}
#[test]
fn test_md060_center_aligned_delimiter_triggers_reformatting() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Header |\n|:-------:|\n| content |\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
!warnings.is_empty(),
"Center-aligned delimiter with left-aligned content should trigger a warning"
);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
let content_line = lines[2];
let trimmed = content_line.trim_start_matches('|');
let cell = trimmed.split('|').next().unwrap();
let left_spaces = cell.len() - cell.trim_start().len();
let right_spaces = cell.len() - cell.trim_end().len();
let diff = left_spaces.abs_diff(right_spaces);
assert!(
diff <= 1,
"Center-aligned content should have balanced padding (diff={diff}): {fixed:?}"
);
}
#[test]
fn test_md060_right_aligned_delimiter_triggers_reformatting() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Header |\n| ------:|\n| data |\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
!warnings.is_empty(),
"Right-aligned delimiter with left-aligned content should trigger a warning"
);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
let content_line = lines[2];
let trimmed = content_line.trim_start_matches('|');
let cell = trimmed.split('|').next().unwrap();
let left_spaces = cell.len() - cell.trim_start().len();
let right_spaces = cell.len() - cell.trim_end().len();
assert!(
left_spaces >= right_spaces,
"Right-aligned content should have left_pad >= right_pad (left={left_spaces}, right={right_spaces}): {fixed:?}"
);
}
#[test]
fn test_md060_already_centered_content_passes() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Header |\n|:-------:|\n| content |\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx2).unwrap();
assert!(
warnings.is_empty(),
"Already-centered content should not trigger warnings. Fixed content:\n{fixed}\nWarnings: {warnings:?}"
);
}
#[test]
fn test_md060_center_aligned_hyphen_content() {
let rule = MD060TableFormat::new(true, "aligned".to_string());
let content = "| Name | Value |\n|:------:|:-----:|\n| alpha | one |\n| beta-2 | two |\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, MarkdownFlavor::Standard, None);
let fixed2 = rule.fix(&ctx2).unwrap();
assert_eq!(
fixed, fixed2,
"Center alignment fix should be idempotent.\nFirst fix:\n{fixed}\nSecond fix:\n{fixed2}"
);
}
#[test]
fn test_md060_loose_last_column_header_caps_width() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Name | Desc |\n|---|---|\n| A | Short |\n| B | A much longer description |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert!(
lines[0].len() < lines[3].len(),
"Header ({} chars) should be shorter than body row with long content ({} chars) in follow-header mode",
lines[0].len(),
lines[3].len()
);
assert_eq!(
lines[0].len(),
lines[1].len(),
"Header and delimiter should have the same length"
);
}
#[test]
fn test_md060_loose_last_column_body_shorter_than_header() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Name | Description |\n|---|---|\n| A | Hi |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert!(lines[0].contains("Description"), "Header should contain 'Description'");
assert_eq!(
lines[2].len(),
lines[0].len(),
"Body row with short content should be padded to header width"
);
}
#[test]
fn test_md060_loose_last_column_three_columns_exact_output() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content =
"| Name | Status | Desc |\n|---|---|---|\n| Foo | OK | Short |\n| Bar | Err | A much longer description here |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0], "| Name | Status | Desc |");
assert_eq!(lines[1], "| ---- | ------ | ---- |");
assert_eq!(lines[2], "| Foo | OK | Short |");
assert_eq!(lines[3], "| Bar | Err | A much longer description here |");
}
#[test]
fn test_md060_loose_last_column_idempotent() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Name | Desc |\n|---|---|\n| A | Short |\n| B | A much longer description |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, MarkdownFlavor::Standard, None);
let fixed2 = rule.fix(&ctx2).unwrap();
assert_eq!(
fixed, fixed2,
"Loose last column fix should be idempotent.\nFirst:\n{fixed}\nSecond:\n{fixed2}"
);
}
#[test]
fn test_md060_loose_last_column_single_column_follow_header() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Header |\n|---|\n| Short |\n| A very long body cell |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0], "| Header |");
assert_eq!(lines[1], "| ------ |");
}
#[test]
fn test_md060_loose_last_column_with_alignment_markers_follow() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Name | Desc |\n|:---|---:|\n| A | Short |\n| B | Very long content |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert!(
lines[1].contains(":---"),
"Left alignment marker should be preserved in delimiter"
);
assert!(
lines[1].contains("---:"),
"Right alignment marker should be preserved in delimiter"
);
}
#[test]
fn test_md060_loose_last_column_cjk_follow_header() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Name | Info |\n|---|---|\n| A | 日本語のテキスト |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0], "| Name | Info |");
}
#[test]
fn test_md060_loose_last_column_aligned_no_space_style() {
let config = MD060Config {
enabled: true,
style: "aligned-no-space".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content =
"| Name | Status | Desc |\n|---|---|---|\n| Foo | OK | Short |\n| Bar | Err | A much longer description |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0], "| Name | Status | Desc |");
assert_eq!(lines[1], "|------|--------|------|");
assert_eq!(lines[2], "| Foo | OK | Short |");
assert_eq!(lines[3], "| Bar | Err | A much longer description |");
}
#[test]
fn test_md060_loose_last_column_all_body_shorter_than_header() {
let content = "| Name | Description |\n|---|---|\n| A | Hi |\n| B | Hey |";
let config_loose = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let config_strict = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: false,
aligned_delimiter: false,
};
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let rule_loose = MD060TableFormat::from_config_struct(config_loose, default_md013_config(), false);
let rule_strict = MD060TableFormat::from_config_struct(config_strict, default_md013_config(), false);
let fixed_loose = rule_loose.fix(&ctx).unwrap();
let fixed_strict = rule_strict.fix(&ctx).unwrap();
assert_eq!(
fixed_loose, fixed_strict,
"Loose should produce identical output to strict when no body cell exceeds header width.\nLoose:\n{fixed_loose}\nStrict:\n{fixed_strict}"
);
}
#[test]
fn test_md060_loose_last_column_header_only_table() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Name | Description |\n|---|---|";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], "| Name | Description |");
assert_eq!(lines[1], "| ---- | ----------- |");
assert_eq!(lines[0].len(), lines[1].len(), "Header and delimiter should match");
}
#[test]
fn test_md060_loose_last_column_empty_header_last_col() {
let config = MD060Config {
enabled: true,
style: "aligned".to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: true,
aligned_delimiter: false,
};
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Name | |\n|---|---|\n| A | Some content |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines[0].len(), lines[1].len(), "Header and delimiter should match");
assert!(
lines[2].len() > lines[0].len(),
"Body row with content should extend beyond empty-header column"
);
}
fn md060_config_with_aligned_delimiter(style: &str, aligned_delimiter: bool) -> MD060Config {
MD060Config {
enabled: true,
style: style.to_string(),
max_width: LineLength::from_const(0),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: false,
aligned_delimiter,
}
}
#[test]
fn test_md060_compact_aligned_delimiter_pads_dashes_to_header_width() {
let config = md060_config_with_aligned_delimiter("compact", true);
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Character | Meaning |\n| --- | --- |\n| Y | Yes |\n| N | No |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = "| Character | Meaning |\n| --------- | ------- |\n| Y | Yes |\n| N | No |";
assert_eq!(
fixed, expected,
"compact + aligned_delimiter pads delimiter dashes to header text widths and leaves body compact"
);
}
#[test]
fn test_md060_compact_aligned_delimiter_default_false_unchanged() {
let config = md060_config_with_aligned_delimiter("compact", false);
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Character | Meaning |\n| --- | --- |\n| Y | Yes |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = "| Character | Meaning |\n| --- | --- |\n| Y | Yes |";
assert_eq!(fixed, expected, "compact without aligned_delimiter is unchanged");
}
#[test]
fn test_md060_tight_aligned_delimiter_pads_dashes_to_header_width() {
let config = md060_config_with_aligned_delimiter("tight", true);
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "|Character|Meaning|\n|-|-|\n|Y|Yes|\n|N|No|";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = "|Character|Meaning|\n|---------|-------|\n|Y|Yes|\n|N|No|";
assert_eq!(
fixed, expected,
"tight + aligned_delimiter pads delimiter dashes to header widths with no surrounding spaces"
);
}
#[test]
fn test_md060_compact_aligned_delimiter_idempotent() {
let config = md060_config_with_aligned_delimiter("compact", true);
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Character | Meaning |\n| --------- | ------- |\n| Y | Yes |\n| N | No |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(
warnings.len(),
0,
"Already-correct compact + aligned_delimiter table should produce no warnings"
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "Fix on already-correct table is a no-op");
}
#[test]
fn test_md060_tight_aligned_delimiter_idempotent() {
let config = md060_config_with_aligned_delimiter("tight", true);
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "|Character|Meaning|\n|---------|-------|\n|Y|Yes|\n|N|No|";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(
warnings.len(),
0,
"Already-correct tight + aligned_delimiter table should produce no warnings"
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "Fix on already-correct table is a no-op");
}
#[test]
fn test_md060_compact_aligned_delimiter_preserves_alignment_markers() {
let config = md060_config_with_aligned_delimiter("compact", true);
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Character | Number | Meaning |\n| :- | -: | :-: |\n| Y | 1 | Yes |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = "| Character | Number | Meaning |\n| :-------- | -----: | :-----: |\n| Y | 1 | Yes |";
assert_eq!(
fixed, expected,
"Alignment markers preserved when padding delimiter cells to header widths"
);
}
#[test]
fn test_md060_compact_aligned_delimiter_flags_misaligned_delimiter() {
let config = md060_config_with_aligned_delimiter("compact", true);
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Character | Meaning |\n| --- | --- |\n| Y | Yes |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
!warnings.is_empty(),
"Misaligned delimiter row must produce at least one warning under aligned_delimiter"
);
}
#[test]
fn test_md060_aligned_style_ignores_aligned_delimiter() {
let config = md060_config_with_aligned_delimiter("aligned", true);
let rule = MD060TableFormat::from_config_struct(config, default_md013_config(), false);
let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed_with = rule.fix(&ctx).unwrap();
let config_off = md060_config_with_aligned_delimiter("aligned", false);
let rule_off = MD060TableFormat::from_config_struct(config_off, default_md013_config(), false);
let fixed_without = rule_off.fix(&ctx).unwrap();
assert_eq!(
fixed_with, fixed_without,
"aligned style ignores aligned_delimiter (already implies it)"
);
}