use rumdl_lib::config::{Config, MarkdownFlavor};
use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD007ULIndent;
#[test]
fn test_valid_list_indent() {
let rule = MD007ULIndent::default();
let content = "* Item 1\n * Item 2\n * Item 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Expected no warnings for valid indentation, but got {} warnings",
result.len()
);
}
#[test]
fn test_invalid_list_indent() {
let rule = MD007ULIndent::default();
let content = "* Item 1\n * Item 2\n * Item 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
println!("test_invalid_list_indent: result.len() = {}", result.len());
for (i, w) in result.iter().enumerate() {
println!(" warning {}: line={}, column={}", i, w.line, w.column);
}
assert_eq!(result.len(), 2);
assert_eq!(result[0].line, 2);
assert_eq!(result[0].column, 1);
assert_eq!(result[1].line, 3);
assert_eq!(result[1].column, 1);
}
#[test]
fn test_mixed_indentation() {
let rule = MD007ULIndent::default();
let content = "* Item 1\n * Item 2\n * Item 3\n * Item 4";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
println!("test_mixed_indentation: result.len() = {}", result.len());
for (i, w) in result.iter().enumerate() {
println!(" warning {}: line={}, column={}", i, w.line, w.column);
}
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 3);
assert_eq!(result[0].column, 1);
}
#[test]
fn test_fix_indentation() {
let rule = MD007ULIndent::default();
let content = "* Item 1\n * Item 2\n * Item 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
let expected = "* Item 1\n * Item 2\n * Item 3";
assert_eq!(result, expected);
}
#[test]
fn test_md007_in_yaml_code_block() {
let rule = MD007ULIndent::default();
let content = r#"```yaml
repos:
- repo: https://github.com/rvben/rumdl
rev: v0.5.0
hooks:
- id: rumdl-check
```"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"MD007 should not trigger inside a code block, but got warnings: {result:?}"
);
}
#[test]
fn test_blockquoted_list_indent() {
let rule = MD007ULIndent::default();
let content = "> * Item 1\n> * Item 2\n> * Item 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Expected no warnings for valid blockquoted list indentation, but got {result:?}"
);
}
#[test]
fn test_blockquoted_list_invalid_indent() {
let rule = MD007ULIndent::default();
let content = "> * Item 1\n> * Item 2\n> * Item 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
2,
"Expected 2 warnings for invalid blockquoted list indentation, got {result:?}"
);
assert_eq!(result[0].line, 2);
assert_eq!(result[1].line, 3);
}
#[test]
fn test_nested_blockquote_list_indent() {
let rule = MD007ULIndent::default();
let content = "> > * Item 1\n> > * Item 2\n> > * Item 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Expected no warnings for valid nested blockquoted list indentation, but got {result:?}"
);
}
#[test]
fn test_blockquote_list_with_code_block() {
let rule = MD007ULIndent::default();
let content = "> * Item 1\n> * Item 2\n> ```\n> code\n> ```\n> * Item 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"MD007 should not trigger inside a code block within a blockquote, but got warnings: {result:?}"
);
}
mod comprehensive_tests {
use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD007ULIndent;
#[test]
fn test_properly_indented_lists() {
let rule = MD007ULIndent::default();
let test_cases = vec![
"* Item 1\n* Item 2",
"* Item 1\n * Item 1.1\n * Item 1.1.1",
"- Item 1\n - Item 1.1",
"+ Item 1\n + Item 1.1",
"* Item 1\n * Item 1.1\n* Item 2\n * Item 2.1",
];
for content in test_cases {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Expected no warnings for properly indented list:\n{}\nGot {} warnings",
content,
result.len()
);
}
}
#[test]
fn test_under_indented_lists() {
let rule = MD007ULIndent::default();
let test_cases = vec![
("* Item 1\n * Item 1.1", 1, 2), ("* Item 1\n * Item 1.1\n * Item 1.1.1", 1, 3), ("- Item 1\n- Item 1.1\n - Item 1.1.1", 0, 0), ];
for (content, expected_warnings, line) in test_cases {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
expected_warnings,
"Expected {expected_warnings} warnings for under-indented list:\n{content}"
);
if expected_warnings > 0 {
assert_eq!(result[0].line, line);
}
}
}
#[test]
fn test_over_indented_lists() {
let rule = MD007ULIndent::default();
let test_cases = vec![
("* Item 1\n * Item 1.1", 1, 2), ("* Item 1\n * Item 1.1", 1, 2), ("* Item 1\n * Item 1.1\n * Item 1.1.1", 1, 3), ];
for (content, expected_warnings, line) in test_cases {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
expected_warnings,
"Expected {expected_warnings} warnings for over-indented list:\n{content}"
);
if expected_warnings > 0 {
assert_eq!(result[0].line, line);
}
}
}
#[test]
fn test_nested_lists_correct_indentation() {
let rule = MD007ULIndent::default();
let content = r#"* Level 1
* Level 2
* Level 3
* Level 4
* Level 3 again
* Level 2 again
* Level 1 again"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Expected no warnings for correctly nested list");
}
#[test]
fn test_nested_lists_incorrect_indentation() {
let rule = MD007ULIndent::default();
let content = r#"* Level 1
* Level 2 (wrong)
* Level 3 (wrong)
* Level 2 (correct)
* Level 3 (wrong)"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 3, "Expected 3 warnings for incorrectly nested list");
let fixed = rule.fix(&ctx).unwrap();
let expected = r#"* Level 1
* Level 2 (wrong)
* Level 3 (wrong)
* Level 2 (correct)
* Level 3 (wrong)"#;
assert_eq!(fixed, expected);
}
#[test]
fn test_custom_indent_2_spaces() {
let rule = MD007ULIndent::new(2); let content = "* Item 1\n * Item 2\n * Item 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_custom_indent_3_spaces() {
let rule = MD007ULIndent::new(3);
let correct_content = "* Item 1\n * Item 2\n * Item 3";
let ctx = LintContext::new(correct_content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Fixed style expects 0, 3, 6 spaces");
let wrong_content = "* Item 1\n * Item 2\n * Item 3";
let ctx = LintContext::new(wrong_content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should warn: expected 3 spaces, found 2");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "* Item 1\n * Item 2\n * Item 3");
}
#[test]
fn test_custom_indent_4_spaces() {
let rule = MD007ULIndent::new(4);
let correct_content = "* Item 1\n * Item 2\n * Item 3";
let ctx = LintContext::new(correct_content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Fixed style expects 0, 4, 8 spaces");
let wrong_content = "* Item 1\n * Item 2\n * Item 3";
let ctx = LintContext::new(wrong_content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should warn: expected 4 spaces, found 2");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "* Item 1\n * Item 2\n * Item 3");
}
#[test]
fn test_tab_indentation() {
let rule = MD007ULIndent::default();
let content = "* Item 1\n\t* Item 2";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Tab indentation should trigger warning");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "* Item 1\n * Item 2");
let content_multi = "* Item 1\n\t* Item 2\n\t\t* Item 3";
let ctx = LintContext::new(content_multi, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "* Item 1\n * Item 2\n * Item 3");
let content_mixed = "* Item 1\n \t* Item 2\n\t * Item 3";
let ctx = LintContext::new(content_mixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "* Item 1\n * Item 2\n * Item 3");
}
#[test]
fn test_mixed_ordered_unordered_lists() {
let rule = MD007ULIndent::default();
let content = r#"1. Ordered item
* Unordered sub-item (wrong indent - only 2 spaces)
2. Ordered sub-item
* Unordered item
1. Ordered sub-item
* Unordered sub-item"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Only unordered list indentation should be checked");
assert_eq!(result[0].line, 2, "Error should be on line 2");
let fixed = rule.fix(&ctx).unwrap();
let expected = r#"1. Ordered item
* Unordered sub-item (wrong indent - only 2 spaces)
2. Ordered sub-item
* Unordered item
1. Ordered sub-item
* Unordered sub-item"#;
assert_eq!(fixed, expected);
}
#[test]
fn test_lists_in_blockquotes_comprehensive() {
let rule = MD007ULIndent::default();
let content1 = "> * Item 1\n> * Item 2\n> * Item 3";
let ctx = LintContext::new(content1, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(rule.check(&ctx).unwrap().is_empty());
let content2 = "> * Item 1\n> * Item 2\n> * Item 3";
let ctx = LintContext::new(content2, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "> * Item 1\n> * Item 2\n> * Item 3");
let content3 = "> > * Item 1\n> > * Item 2\n> > * Item 3";
let ctx = LintContext::new(content3, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(rule.check(&ctx).unwrap().is_empty());
let content4 = "* Regular item\n> * Blockquote item\n> * Nested in blockquote\n* Another regular";
let ctx = LintContext::new(content4, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(rule.check(&ctx).unwrap().is_empty());
}
#[test]
fn test_empty_list_items() {
let rule = MD007ULIndent::default();
let content = "* Item 1\n* \n * Item 2";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Empty list items should not affect indentation checks"
);
}
#[test]
fn test_list_with_code_blocks() {
let rule = MD007ULIndent::default();
let content = r#"* Item 1
```
code
```
* Item 2
* Item 3"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_list_in_front_matter() {
let rule = MD007ULIndent::default();
let content = r#"---
tags:
- tag1
- tag2
---
* Item 1
* Item 2"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Lists in YAML front matter should be ignored");
}
#[test]
fn test_fix_preserves_content() {
let rule = MD007ULIndent::default();
let content = "* Item 1 with **bold** and *italic*\n * Item 2 with `code`\n * Item 3 with [link](url)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = "* Item 1 with **bold** and *italic*\n * Item 2 with `code`\n * Item 3 with [link](url)";
assert_eq!(fixed, expected, "Fix should only change indentation, not content");
}
#[test]
fn test_deeply_nested_lists() {
let rule = MD007ULIndent::default();
let content = r#"* L1
* L2
* L3
* L4
* L5
* L6"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let wrong_content = r#"* L1
* L2
* L3
* L4
* L5
* L6"#;
let ctx = LintContext::new(wrong_content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Deep nesting errors should be detected");
}
#[test]
fn test_list_markers_variety() {
let rule = MD007ULIndent::default();
let content = r#"* Asterisk
* Nested asterisk
- Hyphen
- Nested hyphen
+ Plus
+ Nested plus"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"All unordered list markers should work with proper indentation"
);
let wrong_content = r#"* Asterisk
* Wrong asterisk
- Hyphen
- Wrong hyphen
+ Plus
+ Wrong plus"#;
let ctx = LintContext::new(wrong_content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 3, "All marker types should be checked for indentation");
}
}
mod parity_with_markdownlint {
use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD007ULIndent;
#[test]
fn parity_flat_list_default_indent() {
let input = "* Item 1\n* Item 2\n* Item 3";
let expected = "* Item 1\n* Item 2\n* Item 3";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
assert!(rule.check(&ctx).unwrap().is_empty());
}
#[test]
fn parity_nested_list_default_indent() {
let input = "* Item 1\n * Nested 1\n * Nested 2";
let expected = "* Item 1\n * Nested 1\n * Nested 2";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
assert!(rule.check(&ctx).unwrap().is_empty());
}
#[test]
fn parity_nested_list_incorrect_indent() {
let input = "* Item 1\n * Nested 1\n * Nested 2";
let expected = "* Item 1\n* Nested 1\n * Nested 2";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 2); }
#[test]
fn parity_mixed_markers() {
let input = "* Item 1\n - Nested 1\n + Nested 2";
let expected = "* Item 1\n - Nested 1\n + Nested 2";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
assert!(rule.check(&ctx).unwrap().is_empty());
}
#[test]
fn parity_blockquote_list() {
let input = "> * Item 1\n> * Nested";
let expected = "> * Item 1\n> * Nested";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
assert!(rule.check(&ctx).unwrap().is_empty());
}
#[test]
fn parity_tabs_for_indent() {
let input = "* Item 1\n\t* Nested";
let expected = "* Item 1\n * Nested";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
}
#[test]
fn parity_code_block_ignored() {
let input = "```\n* Not a list\n * Not a nested list\n```\n* Item 1";
let expected = "```\n* Not a list\n * Not a nested list\n```\n* Item 1";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
assert!(rule.check(&ctx).unwrap().is_empty());
}
#[test]
fn parity_custom_indent_4() {
let input = "* Item 1\n * Nested 1\n * Nested 2";
let expected = "* Item 1\n * Nested 1\n * Nested 2";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::new(4);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
}
#[test]
fn parity_empty_input() {
let input = "";
let expected = "";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
assert!(rule.check(&ctx).unwrap().is_empty());
}
#[test]
fn parity_no_lists() {
let input = "# Heading\nSome text";
let expected = "# Heading\nSome text";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
assert!(rule.check(&ctx).unwrap().is_empty());
}
#[test]
fn parity_list_with_blank_lines_between_items() {
let input = "* Item 1\n\n* Item 2\n\n * Nested item 1\n\n * Nested item 2\n* Item 3";
let expected = "* Item 1\n\n* Item 2\n\n * Nested item 1\n\n * Nested item 2\n* Item 3";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed, expected,
"Nested items should maintain proper indentation even after blank lines"
);
}
#[test]
fn parity_list_items_with_trailing_whitespace() {
let input = "* Item 1 \n * Nested item 1 \n* Item 2 ";
let expected = "* Item 1 \n * Nested item 1 \n* Item 2 ";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
}
#[test]
fn parity_deeply_nested_blockquotes_with_lists() {
let input = "> > * Item 1\n> > * Nested item 1\n> > * Nested item 2\n> > * Item 2";
let expected = "> > * Item 1\n> > * Nested item 1\n> > * Nested item 2\n> > * Item 2";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
}
#[test]
fn parity_inconsistent_marker_styles_different_nesting() {
let input = "* Item 1\n - Nested item 1\n + Nested item 2\n* Item 2";
let expected = "* Item 1\n - Nested item 1\n + Nested item 2\n* Item 2";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
}
#[test]
fn parity_mixed_tabs_and_spaces_in_indentation() {
let input = "* Item 1\n\t* Nested item 1\n \t* Nested item 2\n* Item 2";
let expected = "* Item 1\n * Nested item 1\n * Nested item 2\n* Item 2";
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD007ULIndent::default();
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected);
}
}
mod excessive_indentation_bug_fix {
use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD007ULIndent;
#[test]
fn test_md007_excessive_indentation_detection() {
let test =
"- Formatter:\n - The stable style changed\n- Language server:\n - An existing capability is removed";
let rule = MD007ULIndent::default();
let ctx = LintContext::new(test, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 1, "Should detect excessive indentation on line 2");
assert_eq!(warnings[0].line, 2);
assert!(warnings[0].message.contains("Expected 2 spaces"));
assert!(warnings[0].message.contains("found 5"));
}
#[test]
fn test_md007_list_items_not_code_blocks() {
let test = "# Test\n\n- Item 1\n - Item 2 with 4 spaces\n - Item 3 with 5 spaces\n - Item 4 with 6 spaces\n - Item 5 with 8 spaces";
let rule = MD007ULIndent::default();
let ctx = LintContext::new(test, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(warnings.len() >= 2, "Should detect indentation issues on lines 4 and 5");
for warning in &warnings {
assert!(
warning.message.contains("spaces"),
"Should be list indentation warnings"
);
}
}
#[test]
fn test_md007_deeply_nested_lists_vs_code_blocks() {
let test = "# Document\n\n- Top level list\n - 8 spaces (should be 2)\n - 12 spaces (should be 4)\n\nRegular paragraph.\n\n This is an actual code block (4 spaces, not a list)\n It continues here";
let rule = MD007ULIndent::default();
let ctx = LintContext::new(test, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"Expected no MD007 warnings (deeply indented lines are code blocks, not lists), got: {warnings:?}"
);
}
#[test]
fn test_md007_with_4_space_config() {
let test = "- Item 1\n - Item 2 with 4 spaces\n - Item 3 with 5 spaces\n - Item 4 with 6 spaces\n - Item 5 with 8 spaces";
let rule = MD007ULIndent::new(4);
let ctx = LintContext::new(test, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(
warnings.len(),
2,
"Should detect indentation issues on lines 3 and 5, got: {warnings:?}"
);
assert!(warnings.iter().any(|w| w.line == 3), "Line 3 should have warning");
assert!(warnings.iter().any(|w| w.line == 5), "Line 5 should have warning");
}
#[test]
fn test_md007_excessive_indentation_fix() {
let test = "- Item 1\n - Item 2 with 5 spaces\n - Item 3 with 7 spaces";
let rule = MD007ULIndent::default();
let ctx = LintContext::new(test, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(
warnings.len(),
2,
"Should detect excessive indentation on lines 2 and 3"
);
assert_eq!(warnings[0].line, 2);
assert_eq!(warnings[1].line, 3);
let fixed = rule.fix(&ctx).unwrap();
let expected = "- Item 1\n - Item 2 with 5 spaces\n - Item 3 with 7 spaces";
assert_eq!(fixed, expected, "Should fix excessive indentation to correct levels");
}
#[test]
fn test_md007_not_triggered_by_actual_code_blocks() {
let test = "Regular paragraph.\n\n This is a code block\n with multiple lines\n all indented with 4 spaces\n\n- List after code block\n - Properly indented";
let rule = MD007ULIndent::default();
let ctx = LintContext::new(test, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(warnings.is_empty(), "Code blocks should not trigger MD007");
}
mod issue210_indent_config {
use std::fs;
use std::process::Command;
use tempfile::tempdir;
#[test]
fn test_indent_4_pure_unordered() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("repro.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* some
* list
* items
"#;
let config = r#"[global]
line-length = 120
[MD007]
indent = 4
start-indented = false
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"Issue #210: With indent=4, pure unordered lists should use fixed style (0, 4, 8 spaces).\n\
The 4-space indent for level 1 items should be correct.\n\
stdout: {stdout}\n\
stderr: {stderr}\n\
exit code: {exit_code}\n\
If this fails, the indent=4 config is being ignored."
);
}
#[test]
fn test_indent_4_deep_nesting() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* Level 0
* Level 1 (4 spaces)
* Level 2 (8 spaces)
"#;
let config = r#"[MD007]
indent = 4
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"With indent=4 fixed style, 0/4/8 spaces should be correct.\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_indent_4_detects_wrong_indent() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* Level 0
* Level 1 (2 spaces - WRONG, should be 4)
"#;
let config = r#"[MD007]
indent = 4
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("MD007") && stdout.contains("Expected 4 spaces"),
"Should detect wrong indentation (2 spaces instead of 4).\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_explicit_fixed_style() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* Level 0
* Level 1 (4 spaces)
* Level 2 (8 spaces)
"#;
let config = r#"[MD007]
indent = 4
style = "fixed"
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"With explicit style=fixed and indent=4, should work correctly.\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
}
mod issue209_fix_convergence {
use std::fs;
use std::process::Command;
use tempfile::tempdir;
#[test]
fn test_mixed_list_single_pass_convergence() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Header 1
- **First item**:
- First subitem
- Second subitem
- Third subitem
- **Second item**:
- **This is a nested list**:
1. **First point**
- First subpoint
- Second subpoint
- Third subpoint
2. **Second point**
- First subpoint
- Second subpoint
- Third subpoint
"#;
let config = r#"[global]
enable = ["MD005", "MD007"]
[MD007]
indent = 3
style = "text-aligned"
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output1 = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("fmt")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout1 = String::from_utf8_lossy(&output1.stdout);
let output2 = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("fmt")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout2 = String::from_utf8_lossy(&output2.stdout);
assert!(
stdout2.contains("No issues found"),
"Fix should converge in single pass.\n\
First run output:\n{stdout1}\n\
Second run output:\n{stdout2}"
);
}
#[test]
fn test_check_fix_single_pass() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Header 1
- **Second item**:
- **This is a nested list**:
1. **First point**
- First subpoint
2. **Second point**
- First subpoint
"#;
let config = r#"[global]
enable = ["MD005", "MD007"]
[MD007]
indent = 3
style = "text-aligned"
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output1 = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--fix")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let output2 = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout2 = String::from_utf8_lossy(&output2.stdout);
let exit_code = output2.status.code().unwrap_or(-1);
assert!(
stdout2.contains("No issues found") && exit_code == 0,
"After check --fix, no issues should remain.\n\
First run: {:?}\n\
Second run stdout: {stdout2}\n\
Exit code: {exit_code}",
String::from_utf8_lossy(&output1.stdout)
);
}
#[test]
fn test_explicit_text_aligned_no_issues() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Header 1
- **Second item**:
- **This is a nested list**:
1. **First point**
- First subpoint
"#;
let config = r#"[global]
enable = ["MD005", "MD007"]
[MD007]
indent = 3
style = "text-aligned"
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"With explicit text-aligned style, mixed lists should have no issues.\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_default_style_is_text_aligned() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Header 1
- **Second item**:
- **This is a nested list**:
1. **First point**
- First subpoint
"#;
let config = r#"[global]
enable = ["MD005", "MD007"]
[MD007]
indent = 3
style = "text-aligned"
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"Default style should be text-aligned, not auto-switching to fixed.\n\
stdout: {stdout}\n\
exit code: {exit_code}\n\
(If this fails, the auto-switch to fixed style may still be active)"
);
}
}
}
mod indent_config_edge_cases {
use std::fs;
use std::process::Command;
use tempfile::tempdir;
#[test]
fn test_indent_3_pure_unordered() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* Level 0
* Level 1 (3 spaces)
* Level 2 (6 spaces)
"#;
let config = r#"[MD007]
indent = 3
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"With indent=3, pure unordered lists should use fixed style (0, 3, 6 spaces).\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_indent_5_pure_unordered() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* Level 0
* Level 1 (5 spaces)
* Level 2 (10 spaces)
"#;
let config = r#"[MD007]
indent = 5
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"With indent=5, pure unordered lists should use fixed style (0, 5, 10 spaces).\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_indent_4_mixed_lists_text_aligned() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* Unordered item
* Nested unordered (4 spaces - configured indent)
1. Ordered child
* Deeply nested bullet (text-aligned with ordered)
"#;
let config = r#"[global]
disable = ["MD004"]
[MD007]
indent = 4
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"With indent=4, bullets under unordered should use 4-space indent.\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_issue_236_indent_config_respected_in_mixed_docs() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Some Heading
- one item
- another item
- another item
## Heading
1. Some Text.
- a bullet list inside a numbered list.
2. Hello World.
"#;
let config = r#"[ul-indent]
indent = 4
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"Issue #236: indent=4 should be respected for pure unordered sections.\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_indent_4_from_pyproject() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join("pyproject.toml");
let content = r#"# Title
* some
* list
* items
"#;
let config = r#"[tool.rumdl.MD007]
indent = 4
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"Config from pyproject.toml should work correctly.\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_indent_4_explicit_fixed_overrides_auto() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* Unordered
* Nested (4 spaces - fixed style)
"#;
let config = r#"[MD007]
indent = 4
style = "fixed"
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"Explicit style=fixed with correct 4-space indent should be valid.\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_indent_4_explicit_text_aligned() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* Unordered
* Nested (2 spaces - text-aligned)
"#;
let config = r#"[MD007]
indent = 4
style = "text-aligned"
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"Explicit style=text-aligned should work correctly.\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_indent_1_pure_unordered() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* Level 0
* Level 1 (1 space - correct for indent=1 fixed style)
* Level 2 (2 spaces - correct for indent=1 fixed style)
"#;
let config = r#"[global]
disable = ["MD005"]
[MD007]
indent = 1
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"With indent=1, pure unordered lists should use fixed style (0, 1, 2 spaces).\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_indent_8_pure_unordered() {
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* Level 0
* Level 1 (8 spaces)
* Level 2 (16 spaces)
"#;
let config = r#"[MD007]
indent = 8
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(temp_dir.path())
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"With indent=8, pure unordered lists should use fixed style (0, 8, 16 spaces).\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
#[test]
fn test_indent_4_config_in_parent() {
let temp_dir = tempdir().unwrap();
let sub_dir = temp_dir.path().join("sub");
fs::create_dir_all(&sub_dir).unwrap();
let test_file = sub_dir.join("test.md");
let config_file = temp_dir.path().join(".rumdl.toml");
let content = r#"# Title
* some
* list
* items
"#;
let config = r#"[MD007]
indent = 4
"#;
fs::write(&test_file, content).unwrap();
fs::write(&config_file, config).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.arg("check")
.arg("--no-cache")
.current_dir(&sub_dir)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or(-1);
assert!(
stdout.contains("No issues found") && exit_code == 0,
"Config from parent directory should be discovered and used.\n\
stdout: {stdout}\n\
exit code: {exit_code}"
);
}
}
mod issue247_nested_unordered_in_ordered {
use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD007ULIndent;
#[test]
fn test_nested_unordered_in_ordered_no_false_positives() {
let rule = MD007ULIndent::default();
let content = r#"# Header
1. First
- Abc abc
2. Second
- Abc abc
- Xyz
- Aaa
- Bbb
3. Third
- Thirty one
- Hello
- World
- Thirty two
- One
- More
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"Nested unordered lists in ordered lists should not trigger MD007.\n\
markdownlint-cli shows no errors for this structure.\n\
Got {} warnings: {:?}",
warnings.len(),
warnings
);
}
#[test]
fn test_fix_preserves_nested_structure() {
let rule = MD007ULIndent::default();
let content = r#"1. First
- Xyz
- Aaa
- Bbb
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "Fix should not modify already-correct nested structure");
}
#[test]
fn test_simple_unordered_under_ordered() {
let rule = MD007ULIndent::default();
let content = r#"1. Ordered item
- Bullet under ordered
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(warnings.is_empty(), "Bullet at 3 spaces under '1. ' should be valid");
}
#[test]
fn test_double_digit_ordered_list() {
let rule = MD007ULIndent::default();
let content = r#"10. Item ten
- sub
11. Item eleven
- sub
12. Item twelve
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(warnings.is_empty(), "Bullets at 4 spaces under '10. ' should be valid");
}
#[test]
fn test_parent_content_column_used() {
let rule = MD007ULIndent::default();
let content = r#"1. First
- Xyz
- Child at 5 spaces
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"Child at parent's content column (5) should be valid, not nesting_level × indent (4)"
);
}
#[test]
fn test_deeply_nested_mixed_lists() {
let rule = MD007ULIndent::default();
let content = r#"1. Level 1 ordered
- Level 2 unordered (3 spaces)
- Level 3 unordered (5 spaces)
- Level 4 unordered (7 spaces)
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(warnings.is_empty(), "Deeply nested mixed lists should work correctly");
}
}
fn mkdocs_config() -> Config {
let mut config = Config::default();
config.global.flavor = MarkdownFlavor::MkDocs;
config
}
fn mkdocs_config_with_indent(indent: i64) -> Config {
let mut config = mkdocs_config();
config.rules.insert(
"MD007".to_string(),
rumdl_lib::config::RuleConfig {
severity: None,
values: {
let mut m = std::collections::BTreeMap::new();
m.insert("indent".to_string(), toml::Value::Integer(indent));
m
},
},
);
config
}
#[test]
fn test_mkdocs_flavor_enforces_4_space_indent() {
let config = mkdocs_config();
let rule = MD007ULIndent::from_config(&config);
let content = "- text\n - indented\n";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"MkDocs flavor should accept 4-space indent, got: {result:?}"
);
}
#[test]
fn test_mkdocs_flavor_rejects_2_space_indent() {
let config = mkdocs_config();
let rule = MD007ULIndent::from_config(&config);
let content = "- text\n - indented\n";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "MkDocs flavor should reject 2-space indent");
}
#[test]
fn test_mkdocs_flavor_overrides_explicit_indent_2() {
let config = mkdocs_config_with_indent(2);
let rule = MD007ULIndent::from_config(&config);
let content = "- text\n - indented\n";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"MkDocs should enforce indent>=4 even when user sets indent=2, got: {result:?}"
);
let content = "- text\n - indented\n";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"MkDocs should reject 2-space indent even when user sets indent=2"
);
}
#[test]
fn test_mkdocs_flavor_allows_explicit_indent_above_4() {
let config = mkdocs_config_with_indent(6);
let rule = MD007ULIndent::from_config(&config);
let content = "- text\n - indented\n";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"MkDocs with explicit indent=6 should accept 6-space indent, got: {result:?}"
);
}
#[test]
fn test_standard_flavor_keeps_2_space_default() {
let config = Config::default();
let rule = MD007ULIndent::from_config(&config);
let content = "- text\n - indented\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Standard flavor should accept 2-space indent, got: {result:?}"
);
}
#[test]
fn test_mkdocs_flavor_deeply_nested() {
let config = mkdocs_config();
let rule = MD007ULIndent::from_config(&config);
let content = "- level 1\n - level 2\n - level 3\n";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"MkDocs should accept 4-space nested lists, got: {result:?}"
);
}
#[test]
fn test_blockquote_list_in_ordered_list_continuation_issue_526() {
let rule = MD007ULIndent::default();
let content = "\
---
title: Heading
---
1. This is a list item:
> This is a note with a list:
>
> - List item.
> - List item.
- This is a list item.
- This is a list item.
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"List continuation after blockquote should not trigger MD007, got: {result:?}"
);
}
#[test]
fn test_blockquote_list_in_ordered_list_minimal() {
let rule = MD007ULIndent::default();
let content = "\
1. Item
> - Nested in blockquote.
- After blockquote.
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Continuation item after blockquoted list should not be flagged, got: {result:?}"
);
}
#[test]
fn test_blockquote_list_in_unordered_list_continuation() {
let rule = MD007ULIndent::default();
let content = "\
- Parent item
> - Blockquote list item.
> - Another blockquote list item.
- Continuation item.
- Another continuation item.
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Continuation after blockquoted list in unordered list should not be flagged, got: {result:?}"
);
}
#[test]
fn test_multiple_blockquotes_in_list_continuation() {
let rule = MD007ULIndent::default();
let content = "\
1. First item
> - Note list 1.
- Continuation 1.
> - Note list 2.
- Continuation 2.
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Multiple blockquote+continuation cycles should not confuse MD007, got: {result:?}"
);
}
#[test]
fn test_nested_ordered_list_with_blockquote_list() {
let rule = MD007ULIndent::default();
let content = "\
1. Outer item
1. Inner item
> - Blockquote list.
- Continuation of inner.
- Continuation of outer.
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Nested ordered list with blockquoted list should not confuse MD007, got: {result:?}"
);
}
#[test]
fn test_blockquote_with_nested_list_does_not_pollute_parent() {
let rule = MD007ULIndent::default();
let content = "\
1. Item
> - Level 1 in blockquote.
> - Level 2 in blockquote.
> - Level 3 in blockquote.
- Continuation at proper indent.
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Deeply nested blockquote list should not affect parent nesting, got: {result:?}"
);
}
#[test]
fn test_blockquote_list_bad_indent_still_detected() {
let rule = MD007ULIndent::default();
let content = "\
> - Item 1
> - Item 2
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Bad indent inside blockquote should still be caught, got: {result:?}"
);
assert_eq!(result[0].line, 2);
}
#[test]
fn test_blockquote_without_list_then_continuation() {
let rule = MD007ULIndent::default();
let content = "\
1. Item
> Just a blockquote, no list.
- Continuation item.
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Blockquote without list should not affect continuation, got: {result:?}"
);
}
#[test]
fn test_list_in_footnote_definition_not_flagged() {
let rule = MD007ULIndent::default();
let content = "\
# Test
Text.[^note]
[^note]:
- First item
- Second item
- Nested item
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"MD007 should not flag list items inside footnote definitions: {result:?}"
);
}
mod issue541_start_indented_mixed_lists {
use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD007ULIndent;
use rumdl_lib::rules::md007_ul_indent::md007_config::{IndentStyle, MD007Config};
use rumdl_lib::types::IndentSize;
fn start_indented_rule() -> MD007ULIndent {
MD007ULIndent::from_config_struct(MD007Config {
indent: IndentSize::from_const(2),
start_indented: true,
start_indent: IndentSize::from_const(2),
style: IndentStyle::TextAligned,
style_explicit: false,
indent_explicit: false,
})
}
#[test]
fn test_ul_under_ol_with_start_indented() {
let rule = start_indented_rule();
let content = "\
# Le Title
1. First thing
* Sub thing
";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"UL at col 5 under OL '1. ' content col should not trigger MD007.\n\
Got {} warnings: {:?}",
warnings.len(),
warnings
);
}
#[test]
fn test_fix_does_not_break_ol_ul_nesting() {
let rule = start_indented_rule();
let content = "\
# Le Title
1. First thing
* Sub thing
";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed, content,
"Fix must not alter already-correct text-aligned nesting"
);
}
#[test]
fn test_pure_ul_start_indented_no_regression() {
let rule = start_indented_rule();
let content = " * Level 0 (2 spaces)\n * Level 1 (4 spaces)\n * Level 2 (6 spaces)\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"Pure UL with start_indented should accept 2/4/6 spacing.\n\
Got {} warnings: {:?}",
warnings.len(),
warnings
);
}
#[test]
fn test_deeply_nested_ol_ul_ul_start_indented() {
let rule = start_indented_rule();
let content = " 1. First\n * Sub\n * SubSub\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"Deeply nested OL > UL > UL with text-aligned indentation should be valid.\n\
Got {} warnings: {:?}",
warnings.len(),
warnings
);
}
#[test]
fn test_multi_digit_ol_ul_start_indented() {
let rule = start_indented_rule();
let content = " 10. Ten\n * Sub\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"Multi-digit OL with start_indented should use text-aligned indent.\n\
Got {} warnings: {:?}",
warnings.len(),
warnings
);
}
#[test]
fn test_ul_ol_ul_mixed_start_indented() {
let rule = start_indented_rule();
let content = " * Parent\n 1. Ordered child\n * Grandchild\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"UL > OL > UL mixed nesting with start_indented should be valid.\n\
Got {} warnings: {:?}",
warnings.len(),
warnings
);
}
#[test]
fn test_fix_idempotency_start_indented() {
let rule = start_indented_rule();
let content = " 1. First thing\n * Sub thing\n * Deep thing\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed1 = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed1, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed2 = rule.fix(&ctx2).unwrap();
assert_eq!(
fixed1, fixed2,
"Fix must be idempotent (second run should produce no changes)"
);
}
#[test]
fn test_start_indent_differs_from_indent() {
let rule = MD007ULIndent::from_config_struct(MD007Config {
indent: IndentSize::from_const(2),
start_indented: true,
start_indent: IndentSize::from_const(4),
style: IndentStyle::TextAligned,
style_explicit: false,
indent_explicit: false,
});
let content = " 1. Item\n * Sub\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"With start_indent=4, indent=2: UL at col 7 text-aligned under OL should be valid.\n\
Got {} warnings: {:?}",
warnings.len(),
warnings
);
}
#[test]
fn test_wrong_indent_under_ol_still_warns() {
let rule = start_indented_rule();
let content = " 1. First\n * Sub\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
!warnings.is_empty(),
"UL at col 3 under OL content col 5 should trigger MD007 with start_indented"
);
}
}
#[test]
fn test_math_block_operators_not_flagged_by_md007() {
let rule = MD007ULIndent::default();
let content = "\
# Example math
$$
- \\operatorname{Re} \\frac{L'(s, \\chi)}{L(s, \\chi)}
+ \\frac{1}{2} \\log\\frac{q}{\\pi}
$$
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"MD007 should not flag math operators inside $$ blocks as list items: {warnings:?}"
);
}
#[test]
fn test_math_block_indented_operators_not_flagged_by_md007() {
let rule = MD007ULIndent::default();
let content = "\
# Math
$$
- a
- b
- c
$$
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
warnings.is_empty(),
"MD007 should not flag indented lines inside math blocks: {warnings:?}"
);
}
#[test]
fn test_real_list_after_math_block_still_checked_by_md007() {
let rule = MD007ULIndent::default();
let content = "\
# Example
$$
- \\operatorname{Re}
$$
- Item 1
- Nested with wrong indent
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(
!warnings.is_empty(),
"MD007 should still flag real list items with wrong indentation outside math blocks"
);
}