use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD010NoHardTabs;
#[test]
fn test_no_hard_tabs() {
let rule = MD010NoHardTabs::default();
let content = "This line is fine\n Indented with spaces";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_content_with_no_tabs_various_contexts() {
let rule = MD010NoHardTabs::default();
let content = "# Heading without tabs\n\n Indented with spaces\n\n- List item\n - Nested with spaces\n\n```\nCode without tabs\n```";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Content with only spaces should pass");
}
#[test]
fn test_leading_hard_tabs() {
let rule = MD010NoHardTabs::default();
let content = "\tIndented line\n\t\tDouble indented";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2); assert_eq!(result[0].line, 1);
assert_eq!(result[0].message, "Found leading tab, use 4 spaces instead");
assert_eq!(result[1].line, 2);
assert_eq!(result[1].message, "Found 2 leading tabs, use 8 spaces instead");
}
#[test]
fn test_alignment_tabs() {
let rule = MD010NoHardTabs::default();
let content = "Text with\ttab for alignment";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 1);
assert_eq!(result[0].message, "Found tab for alignment, use spaces instead");
}
#[test]
fn test_empty_line_tabs() {
let rule = MD010NoHardTabs::default();
let content = "Normal line\n\t\t\n\tMore text";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2); assert_eq!(result[0].line, 2);
assert_eq!(result[0].message, "Empty line contains 2 tabs");
assert_eq!(result[1].line, 3);
assert_eq!(result[1].message, "Found leading tab, use 4 spaces instead");
}
#[test]
fn test_code_blocks_allowed() {
let rule = MD010NoHardTabs::new(4);
let content = "Normal line\n```\n\tCode with tab\n\tMore code\n```\nNormal\tline";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1); assert_eq!(result[0].line, 6);
}
#[test]
fn test_code_blocks_not_allowed() {
let rule = MD010NoHardTabs::default(); let content = "Normal line\n```\n\tCode with tab\n\tMore code\n```\nNormal\tline";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1); assert_eq!(result[0].line, 6);
}
#[test]
fn test_fix_with_code_blocks() {
let rule = MD010NoHardTabs::new(2); let content = "\tIndented line\n```\n\tCode\n```\n\t\tDouble indented";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, " Indented line\n```\n\tCode\n```\n Double indented");
}
#[test]
fn test_fix_without_code_blocks() {
let rule = MD010NoHardTabs::new(2); let content = "\tIndented line\n```\n\tCode\n```\n\t\tDouble indented";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, " Indented line\n```\n\tCode\n```\n Double indented");
}
#[test]
fn test_mixed_indentation() {
let rule = MD010NoHardTabs::default();
let content = " Spaces\n\tTab\n \tMixed";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].line, 2);
assert_eq!(result[1].line, 3);
}
#[test]
fn test_html_comments_with_tabs() {
let rule = MD010NoHardTabs::default();
let content = "<!-- This comment has a \t tab -->\nNormal line";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "Should ignore tabs in single-line HTML comments");
let content = "<!-- Start of comment\nUser: \t\tuser\nPassword:\tpass\n-->\nNormal\tline";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Should only flag tab in normal line, not in multi-line comment"
);
assert_eq!(result[0].line, 5);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed, "<!-- Start of comment\nUser: \t\tuser\nPassword:\tpass\n-->\nNormal line",
"Should preserve tabs in HTML comments but fix tabs in normal text"
);
}
#[test]
fn test_md010_tabs_in_nested_code_blocks() {
let rule = MD010NoHardTabs::new(4);
let content = "No\ttabs\there\n\n```\n\tTabs\tin\tcode\n```\n\nRegular\ttext\twith\ttabs";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("No tabs here"),
"Tabs outside code should be replaced"
);
assert!(
fixed.contains("\tTabs\tin\tcode"),
"Tabs in fenced code should be preserved"
);
assert!(
fixed.contains("Regular text with tabs"),
"Tabs in regular text should be replaced"
);
}
#[test]
fn test_md010_tabs_in_indented_code() {
let rule = MD010NoHardTabs::new(4);
let content = "Text\n\n\t\tCode with tabs\n\t\tMore code\n\nText\twith\ttab";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains(" Code with tabs"),
"Tabs in tab-indented content should be replaced"
);
assert!(
fixed.contains("Text with tab"),
"Tabs outside code should be replaced"
);
}
#[test]
fn test_md010_mixed_indentation_in_code() {
let rule = MD010NoHardTabs::new(2);
let content = "```python\n spaces\n\ttab\n \tmixed\n```\n\nOutside\ttab";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains(" spaces\n\ttab\n \tmixed"),
"Mixed indentation in code preserved"
);
assert!(fixed.contains("Outside tab"), "Tab outside converted to 2 spaces");
}
#[test]
fn test_interaction_list_code_tabs() {
let content = r#"1. List with tab
```
Code with tab
```
2. Wrong number here"#;
let rule_tabs = MD010NoHardTabs::new(4);
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed_tabs = rule_tabs.fix(&ctx).unwrap();
let expected = r#"1. List with tab
```
Code with tab
```
2. Wrong number here"#;
assert_eq!(
fixed_tabs, expected,
"Tabs in list items should be replaced, code block tabs preserved"
);
}
#[test]
fn test_multiple_tabs_on_same_line() {
let rule = MD010NoHardTabs::default();
let content = "Start\there\tand\there\twith\ttabs";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 5, "Should detect each tab separately");
for warning in result.iter() {
assert_eq!(warning.line, 1);
assert_eq!(warning.message, "Found tab for alignment, use spaces instead");
}
}
#[test]
fn test_tab_character_in_different_positions() {
let rule = MD010NoHardTabs::default();
let content = "\tStart tab\nMiddle\ttab\nEnd tab\t\n\t\tDouble start\nMixed \t \t spaces";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 6, "Should detect all tabs");
assert_eq!(result[0].message, "Found leading tab, use 4 spaces instead");
assert_eq!(result[1].message, "Found tab for alignment, use spaces instead");
assert_eq!(result[2].message, "Found tab for alignment, use spaces instead");
assert_eq!(result[3].message, "Found 2 leading tabs, use 8 spaces instead");
assert_eq!(result[4].message, "Found tab for alignment, use spaces instead");
assert_eq!(result[5].message, "Found tab for alignment, use spaces instead");
}
#[test]
fn test_mixed_tabs_and_spaces_detailed() {
let rule = MD010NoHardTabs::default();
let content =
" \tTwo spaces then tab\n\t Tab then two spaces\n \t \t Space tab space tab\n\t\t Two tabs then spaces";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 5, "Should detect all tabs");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed,
" Two spaces then tab\n Tab then two spaces\n Space tab space tab\n Two tabs then spaces"
);
}
#[test]
fn test_empty_lines_with_only_tabs_variations() {
let rule = MD010NoHardTabs::default();
let content = "\t\n\t\t\n\t\t\t\n\t \t\n \t \t \n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 7, "Should detect all tab groups");
assert_eq!(result[0].message, "Empty line contains tab");
assert_eq!(result[1].message, "Empty line contains 2 tabs");
assert_eq!(result[2].message, "Empty line contains 3 tabs");
assert_eq!(result[3].message, "Empty line contains tab");
assert_eq!(result[4].message, "Empty line contains tab");
assert_eq!(result[5].message, "Empty line contains tab");
assert_eq!(result[6].message, "Empty line contains tab");
}
#[test]
fn test_configuration_spaces_per_tab() {
let content = "\tOne tab\n\t\tTwo tabs\n\t\t\tThree tabs";
let rule2 = MD010NoHardTabs::new(2);
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed2 = rule2.fix(&ctx).unwrap();
assert_eq!(fixed2, " One tab\n Two tabs\n Three tabs");
let rule8 = MD010NoHardTabs::new(8);
let fixed8 = rule8.fix(&ctx).unwrap();
assert_eq!(
fixed8,
" One tab\n Two tabs\n Three tabs"
);
}
#[test]
fn test_configuration_code_blocks_parameter() {
let content = "Normal\ttab\n\n```javascript\nfunction\tfoo() {\n\treturn\ttrue;\n}\n```\n\nAnother\ttab";
let rule = MD010NoHardTabs::new(4);
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Should always skip tabs in code blocks");
assert_eq!(result[0].line, 1);
assert_eq!(result[1].line, 9);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("function\tfoo()"), "Should preserve tabs in code blocks");
assert!(fixed.contains("Normal tab"), "Should fix tabs outside code blocks");
}
#[test]
fn test_consecutive_vs_separate_tabs() {
let rule = MD010NoHardTabs::default();
let content = "\t\t\tThree consecutive\nOne\tthen\tanother\t";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 4, "Should have 1 group for consecutive, 3 separate");
assert_eq!(result[0].message, "Found 3 leading tabs, use 12 spaces instead");
assert_eq!(result[1].message, "Found tab for alignment, use spaces instead");
assert_eq!(result[2].message, "Found tab for alignment, use spaces instead");
assert_eq!(result[3].message, "Found tab for alignment, use spaces instead");
}
#[test]
fn test_fix_preserves_content_structure() {
let rule = MD010NoHardTabs::default();
let content = "# Header\n\n\tIndented paragraph\n\n- List\n\t- Nested\n\t\t- Double nested\n\n```\n\tCode block\n```\n\n> Quote\n> \tWith tab\n\n| Col1\t| Col2\t|\n|---\t|---\t|\n| Data\t| Data\t|";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("# Header"), "Headers preserved");
assert!(
fixed.contains(" Indented paragraph"),
"Tab-indented content converted"
);
assert!(fixed.contains(" - Nested"), "List indentation converted");
assert!(
fixed.contains(" - Double nested"),
"Double indentation converted"
);
assert!(fixed.contains("\tCode block"), "Code block tabs preserved");
assert!(fixed.contains("> With tab"), "Quote tab converted");
assert!(fixed.contains("| Col1 | Col2 |"), "Table tabs converted");
}
#[test]
fn test_edge_cases() {
let rule = MD010NoHardTabs::default();
let content = "\t"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].message, "Empty line contains tab");
let content2 = "Text\t";
let ctx2 = LintContext::new(content2, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result2 = rule.check(&ctx2).unwrap();
assert_eq!(result2.len(), 1);
let fixed2 = rule.fix(&ctx2).unwrap();
assert_eq!(fixed2, "Text ");
assert!(!fixed2.ends_with('\n'), "Should preserve lack of final newline");
}
#[test]
fn test_inline_code_spans() {
let rule = MD010NoHardTabs::new(4);
let content = "Text with `inline\tcode` and\ttab outside";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Should detect tabs in inline code and outside");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Text with `inline code` and tab outside");
}
#[test]
fn test_roundtrip_fix_then_recheck_simple() {
let rule = MD010NoHardTabs::default();
let content = "\tIndented\nNormal\tline\nNo tabs";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx2).unwrap();
assert!(
warnings.is_empty(),
"After fix, re-check should produce 0 warnings but got: {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_then_recheck_code_blocks() {
let rule = MD010NoHardTabs::default();
let content = "Text\twith\ttab\n```makefile\ntarget:\n\tcommand\n```\nMore\ttabs";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx2).unwrap();
assert!(
warnings.is_empty(),
"After fix, re-check should produce 0 warnings but got: {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_then_recheck_custom_spaces() {
let rule = MD010NoHardTabs::new(2);
let content = "\tOne tab\n\t\tTwo tabs\n\t\t\tThree tabs";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx2).unwrap();
assert!(
warnings.is_empty(),
"After fix, re-check should produce 0 warnings but got: {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_then_recheck_mixed_content() {
let rule = MD010NoHardTabs::default();
let content = "# Header\n\n\tIndented paragraph\n\n- List\n\t- Nested\n\t\t- Double nested\n\n```\n\tCode block\n```\n\n> Quote\n> \tWith tab\n\n| Col1\t| Col2\t|\n|---\t|---\t|\n| Data\t| Data\t|";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx2).unwrap();
assert!(
warnings.is_empty(),
"After fix, re-check should produce 0 warnings but got: {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_then_recheck_empty_lines_with_tabs() {
let rule = MD010NoHardTabs::default();
let content = "Normal line\n\t\t\n\t\nAnother line";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx2).unwrap();
assert!(
warnings.is_empty(),
"After fix, re-check should produce 0 warnings but got: {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_then_recheck_html_comments() {
let rule = MD010NoHardTabs::default();
let content = "<!-- Start of comment\nUser: \t\tuser\nPassword:\tpass\n-->\nNormal\tline";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx2).unwrap();
assert!(
warnings.is_empty(),
"After fix, re-check should produce 0 warnings but got: {warnings:?}"
);
}