use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD031BlanksAroundFences;
#[test]
fn test_valid_fenced_blocks() {
let rule = MD031BlanksAroundFences::default();
let content = "Text before\n\n```\ncode block\n```\n\nText after";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_no_blank_before() {
let rule = MD031BlanksAroundFences::default();
let content = "Text before\n```\ncode block\n```\n\nText after";
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, 2);
assert_eq!(result[0].column, 1);
}
#[test]
fn test_no_blank_after() {
let rule = MD031BlanksAroundFences::default();
let content = "Text before\n\n```\ncode block\n```\nText after";
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, 5);
assert_eq!(result[0].column, 1);
}
#[test]
fn test_fix_missing_blanks() {
let rule = MD031BlanksAroundFences::default();
let content = "Text before\n```\ncode block\n```\nText after";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
let fixed_ctx = LintContext::new(&result, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed_result = rule.check(&fixed_ctx).unwrap();
assert_eq!(fixed_result, Vec::new());
}
#[test]
fn test_nested_code_blocks_no_internal_blanks() {
let rule = MD031BlanksAroundFences::default();
let content = "# Test\n\n````markdown\nHere's some text.\n\n```python\ndef hello():\n print(\"Hello!\")\n```\n\nMore text.\n````\n\nAfter.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert!(result.contains("```python\ndef hello():\n print(\"Hello!\")\n```"));
assert!(!result.contains("```python\n\ndef hello()"));
assert!(!result.contains("print(\"Hello!\")\n\n```"));
let lines: Vec<&str> = result.lines().collect();
let markdown_start = lines.iter().position(|&line| line.starts_with("````markdown")).unwrap();
let markdown_end = lines.iter().rposition(|&line| line.starts_with("````")).unwrap();
assert_eq!(lines[markdown_start - 1], "");
assert_eq!(lines[markdown_end + 1], "");
}
#[test]
fn test_nested_code_blocks_different_fence_types() {
let rule = MD031BlanksAroundFences::default();
let content = "Text\n~~~markdown\n```python\ncode\n```\n~~~\nAfter";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert!(result.contains("```python\ncode\n```"));
assert!(!result.contains("```python\n\ncode"));
assert!(!result.contains("code\n\n```"));
}
#[test]
fn test_multiple_nested_levels() {
let rule = MD031BlanksAroundFences::default();
let content = "`````text\n````markdown\n```python\ncode\n```\n````\n`````";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert!(result.contains("````markdown\n```python\ncode\n```\n````"));
assert!(!result.contains("```python\n\ncode"));
}
#[test]
fn test_nested_vs_standalone_distinction() {
let rule = MD031BlanksAroundFences::default();
let content = "# Test\nStandalone:\n```python\ncode1\n```\nNested:\n````markdown\n```python\ncode2\n```\n````\nEnd";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert!(result.contains("Standalone:\n\n```python\ncode1\n```\n\nNested:"));
assert!(result.contains("```python\ncode2\n```"));
assert!(!result.contains("```python\n\ncode2"));
assert!(result.contains("Nested:\n\n````markdown"));
assert!(result.contains("````\n\nEnd"));
}
#[test]
fn test_mixed_fence_markers_nested() {
let rule = MD031BlanksAroundFences::default();
let content = "Test1:\n~~~text\n```python\ncode\n```\n~~~\nTest2:\n````text\n~~~bash\nscript\n~~~\n````\nEnd";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert!(result.contains("```python\ncode\n```"));
assert!(result.contains("~~~bash\nscript\n~~~"));
assert!(!result.contains("```python\n\ncode"));
assert!(!result.contains("~~~bash\n\nscript"));
assert!(result.contains("Test1:\n\n~~~text"));
assert!(result.contains("Test2:\n\n````text"));
}
#[test]
fn test_documentation_example_scenario() {
let rule = MD031BlanksAroundFences::default();
let content = "### Example\n\n````markdown\nHere's some text explaining the code.\n\n```python\ndef hello():\n print(\"Hello, world!\")\n```\n\nAnd here's more text after the code.\n````\n\n## Next section";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert!(result.contains("```python\ndef hello():\n print(\"Hello, world!\")\n```"));
assert!(!result.contains("```python\n\ndef hello()"));
assert!(!result.contains("print(\"Hello, world!\")\n\n```"));
assert!(result.contains("### Example\n\n````markdown"));
assert!(result.contains("````\n\n## Next section"));
}
#[test]
fn test_fence_length_specificity() {
let rule = MD031BlanksAroundFences::default();
let content = "````markdown\n```python\ncode\n```\nmore content\n````";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = result.lines().collect();
let python_line = lines.iter().position(|&line| line == "```python").unwrap();
let close_python_line = lines.iter().position(|&line| line == "```").unwrap();
let more_content_line = lines.iter().position(|&line| line == "more content").unwrap();
assert!(python_line < close_python_line);
assert!(close_python_line < more_content_line);
}
#[test]
fn test_code_blocks_in_lists() {
let rule = MD031BlanksAroundFences::default();
let content = r#"# Test
1. First item with code:
```python
code_in_list()
```
2. Second item
3. Third item with code:
```javascript
console.log("test");
```
More text in item 3.
Regular paragraph."#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Should detect missing blank lines around code blocks in lists"
);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("1. First item with code:\n\n ```python"));
assert!(fixed.contains(" ```\n\n2. Second item"));
assert!(fixed.contains("3. Third item with code:\n\n ```javascript"));
assert!(fixed.contains(" ```\n\n More text"));
}
#[test]
fn test_issue_284_blockquote_blank_lines() {
let rule = MD031BlanksAroundFences::default();
let content = r#"# Blockquote with code
> Some content
>
> ```python
> def hello():
> print("Hello")
> ```
>
> More content
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Empty blockquote lines should be treated as blank lines: {result:?}"
);
}
#[test]
fn test_blockquote_with_blank_marker_only() {
let rule = MD031BlanksAroundFences::default();
let content = "> Text before\n>\n> ```\n> code\n> ```\n>\n> Text after";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Blockquote with > as blank line should not trigger MD031: {result:?}"
);
}
#[test]
fn test_blockquote_with_trailing_space_blank() {
let rule = MD031BlanksAroundFences::default();
let content = "> Text before\n> \n> ```\n> code\n> ```\n> \n> Text after";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Blockquote with '> ' as blank line should not trigger MD031: {result:?}"
);
}
#[test]
fn test_nested_blockquote_blank_lines() {
let rule = MD031BlanksAroundFences::default();
let content = r#">> Nested content
>>
>> ```python
>> code here
>> ```
>>
>> More nested content
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Nested blockquote blank lines should work: {result:?}"
);
}
#[test]
fn test_blockquote_still_detects_missing_blanks() {
let rule = MD031BlanksAroundFences::default();
let content = "> Text before\n> ```\n> code\n> ```\n> Text after";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let _result = rule.check(&ctx).unwrap();
}
#[test]
fn test_roundtrip_fix_then_recheck_basic() {
let rule = MD031BlanksAroundFences::default();
let content = "Text before\n```\ncode block\n```\nText after";
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(),
"Roundtrip (basic): fix then re-check should produce 0 warnings, got {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_then_recheck_list_items() {
let rule = MD031BlanksAroundFences::new(true);
let content = "1. First item\n ```python\n code_in_list()\n ```\n2. Second item";
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(),
"Roundtrip (list items): fix then re-check should produce 0 warnings, got {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_then_recheck_blockquote() {
let rule = MD031BlanksAroundFences::default();
let content = "> Text before\n> ```\n> code\n> ```\n> Text after";
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(),
"Roundtrip (blockquote): fix then re-check should produce 0 warnings, got {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_then_recheck_nested_blockquote() {
let rule = MD031BlanksAroundFences::default();
let content = ">> Nested quote\n>> ```\n>> code\n>> ```\n>> More text";
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(),
"Roundtrip (nested blockquote): fix then re-check should produce 0 warnings, got {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_then_recheck_multiple_blocks() {
let rule = MD031BlanksAroundFences::default();
let content = "Text\n```\ncode1\n```\nMiddle\n```\ncode2\n```\nEnd";
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(),
"Roundtrip (multiple blocks): fix then re-check should produce 0 warnings, got {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_then_recheck_trailing_newline() {
let rule = MD031BlanksAroundFences::default();
let content = "Some text\n```\ncode\n```\nMore text\n";
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(),
"Roundtrip (trailing newline): fix then re-check should produce 0 warnings, got {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_idempotent() {
let rule = MD031BlanksAroundFences::default();
let content = "Text\n```\ncode\n```\nMore";
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 should be idempotent");
}
#[test]
fn test_roundtrip_fix_then_recheck_mkdocs_admonition() {
let rule = MD031BlanksAroundFences::default();
let content = "Text before\n!!! note\n Content\nText after";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::MkDocs, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::MkDocs, None);
let warnings = rule.check(&ctx2).unwrap();
assert!(
warnings.is_empty(),
"Roundtrip (MkDocs admonition): fix then re-check should produce 0 warnings, got {warnings:?}"
);
}
#[test]
fn test_roundtrip_fix_then_recheck_4space_list() {
let rule = MD031BlanksAroundFences::new(true);
let content =
"1. First item\n2. Second item with code:\n ```python\n print(\"Hello\")\n ```\n3. Third item";
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(),
"Roundtrip (4-space list): fix then re-check should produce 0 warnings, got {warnings:?}"
);
}
#[test]
fn test_mixed_blockquote_and_regular_content() {
let rule = MD031BlanksAroundFences::default();
let content = r#"# Mixed Content
> Blockquote with proper spacing
>
> ```python
> inside_quote()
> ```
>
> End of quote
Regular text without blank line
```javascript
outside_quote();
```
More text
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Should still detect missing blanks outside blockquotes"
);
let warning_lines: Vec<usize> = result.iter().map(|w| w.line).collect();
assert!(
warning_lines.iter().all(|&l| l >= 12),
"Warnings should be for non-blockquote section: {warning_lines:?}"
);
}