use super::*;
use crate::config::MarkdownFlavor;
use crate::lint_context::LintContext;
#[test]
fn test_default_config() {
let rule = MD013LineLength::default();
assert_eq!(rule.config.line_length.get(), 80);
assert!(rule.config.code_blocks); assert!(!rule.config.tables); assert!(rule.config.headings); assert!(!rule.config.strict);
}
#[test]
fn test_custom_config() {
let rule = MD013LineLength::new(100, true, true, false, true);
assert_eq!(rule.config.line_length.get(), 100);
assert!(rule.config.code_blocks);
assert!(rule.config.tables);
assert!(!rule.config.headings);
assert!(rule.config.strict);
}
#[test]
fn test_basic_line_length_violation() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "This is a line that is definitely longer than fifty characters and should trigger a warning.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].message.contains("Line length"));
assert!(result[0].message.contains("exceeds 50 characters"));
}
#[test]
fn test_no_violation_under_limit() {
let rule = MD013LineLength::new(100, false, false, false, false);
let content = "Short line.\nAnother short line.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_multiple_violations() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content =
"This line is definitely longer than thirty chars.\nThis is also a line that exceeds the limit.\nShort line.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].line, 1);
assert_eq!(result[1].line, 2);
}
#[test]
fn test_no_lint_front_matter() {
let rule = MD013LineLength::new(80, false, false, false, false);
let content = "---\ntitle: This is a very long title that exceeds eighty characters and should not trigger MD013\nauthor: Another very long line in YAML front matter that exceeds the eighty character limit\n---\n\n# Heading\n\nThis is a very long line in actual content that exceeds eighty characters and SHOULD trigger MD013.\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 8);
let content_toml = "+++\ntitle = \"This is a very long title in TOML that exceeds eighty characters and should not trigger MD013\"\nauthor = \"Another very long line in TOML front matter that exceeds the eighty character limit\"\n+++\n\n# Heading\n\nThis is a very long line in actual content that exceeds eighty characters and SHOULD trigger MD013.\n";
let ctx_toml = LintContext::new(content_toml, crate::config::MarkdownFlavor::Standard, None);
let result_toml = rule.check(&ctx_toml).unwrap();
assert_eq!(result_toml.len(), 1);
assert_eq!(result_toml[0].line, 8); }
#[test]
fn test_code_blocks_exemption() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "```\nThis is a very long line inside a code block that should be ignored.\n```";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_code_blocks_not_exempt_when_configured() {
let rule = MD013LineLength::new(30, true, false, false, false);
let content = "```\nThis is a very long line inside a code block that should NOT be ignored.\n```";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_heading_checked_when_enabled() {
let rule = MD013LineLength::new(30, false, false, true, false);
let content = "# This is a very long heading that would normally exceed the limit";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_heading_exempt_when_disabled() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "# This is a very long heading that should trigger a warning";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_table_checked_when_enabled() {
let rule = MD013LineLength::new(30, false, true, false, false);
let content = "| This is a very long table header | Another long column header |\n|-----------------------------------|-------------------------------|";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 1);
}
#[test]
fn test_issue_78_tables_after_fenced_code_blocks() {
let rule = MD013LineLength::new(20, false, false, false, false); let content = r#"# heading
```plain
some code block longer than 20 chars length
```
this is a very long line
| column A | column B |
| -------- | -------- |
| `var` | `val` |
| value 1 | value 2 |
correct length line"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should only flag 1 line (the non-table long line)");
assert_eq!(result[0].line, 7, "Should flag line 7");
assert!(result[0].message.contains("24 exceeds 20"));
}
#[test]
fn test_issue_78_tables_with_inline_code() {
let rule = MD013LineLength::new(20, false, false, false, false); let content = r#"| column A | column B |
| -------- | -------- |
| `var with very long name` | `val exceeding limit` |
| value 1 | value 2 |
This line has extra words that exceed the limit even after trailing-word forgiveness"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should only flag the non-table line");
assert_eq!(result[0].line, 6, "Should flag line 6");
}
#[test]
fn test_issue_78_indented_code_blocks() {
let rule = MD013LineLength::new(20, false, false, false, false); let content = "# heading
some code block longer than 20 chars length
this is a very long line
| column A | column B |
| -------- | -------- |
| value 1 | value 2 |
correct length line";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should only flag 1 line (the non-table long line)");
assert_eq!(result[0].line, 5, "Should flag line 5");
}
#[test]
fn test_url_exemption() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "https://example.com/this/is/a/very/long/url/that/exceeds/the/limit";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_image_reference_exemption() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "![This is a very long image alt text that exceeds limit][reference]";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_link_reference_exemption() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "[reference]: https://example.com/very/long/url/that/exceeds/limit";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_strict_mode() {
let rule = MD013LineLength::new(30, false, false, false, true);
let content = "https://example.com/this/is/a/very/long/url/that/exceeds/the/limit";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_blockquote_wrappable_text_is_flagged() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "> This is a very long line inside a blockquote that should be flagged.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_setext_heading_underline_exemption() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "Heading\n========================================";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_no_fix_without_reflow() {
let rule = MD013LineLength::new(60, false, false, false, false);
let content = "This line has trailing whitespace that makes it too long ";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].fix.is_none());
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content);
}
#[test]
fn test_character_vs_byte_counting() {
let rule = MD013LineLength::new(10, false, false, false, true);
let content = "你好世界这是测试文字超过限制"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 1);
}
#[test]
fn test_empty_content() {
let rule = MD013LineLength::default();
let ctx = LintContext::new("", crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_excess_range_calculation() {
let rule = MD013LineLength::new(10, false, false, false, true);
let content = "12345678901234567890"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].column, 11);
assert_eq!(result[0].end_column, 21);
}
#[test]
fn test_html_block_exemption() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "<div>\nThis is a very long line inside an HTML block that should be ignored.\n</div>";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_mixed_content() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = r#"# This heading is very long but should be exempt
This regular paragraph line is too long and should trigger.
```
Code block line that is very long but exempt.
```
| Table | With very long content |
|-------|------------------------|
Another long line that should trigger a warning."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].line, 3);
assert_eq!(result[1].line, 12);
}
#[test]
fn test_fix_without_reflow_preserves_content() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "Line 1\nThis line has trailing spaces and is too long \nLine 3";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content);
}
#[test]
fn test_content_detection() {
let rule = MD013LineLength::default();
let long_line = "a".repeat(100);
let ctx = LintContext::new(&long_line, crate::config::MarkdownFlavor::Standard, None);
assert!(!rule.should_skip(&ctx));
let empty_ctx = LintContext::new("", crate::config::MarkdownFlavor::Standard, None);
assert!(rule.should_skip(&empty_ctx)); }
#[test]
fn test_rule_metadata() {
let rule = MD013LineLength::default();
assert_eq!(rule.name(), "MD013");
assert_eq!(rule.description(), "Line length should not be excessive");
assert_eq!(rule.category(), RuleCategory::Whitespace);
}
#[test]
fn test_url_embedded_in_text() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "Check the docs at https://example.com/very/long/url/that/exceeds/limit for info";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_multiple_urls_in_line() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "See https://first-url.com/long and https://second-url.com/also/very/long here";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_markdown_link_with_long_url() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "Check the [documentation](https://example.com/very/long/path/to/documentation/page) for details";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_line_too_long_even_without_urls() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "This is a very long line with lots of text and https://url.com that still exceeds the limit";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_strict_mode_counts_urls() {
let rule = MD013LineLength::new(50, false, false, false, true);
let content = "Check the docs at https://example.com/very/long/url/that/exceeds/limit for info";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_trailing_link_forgiven_in_non_strict() {
let rule = MD013LineLength::new(80, false, false, false, false);
let content = r#"For more information, see the [CommonMark specification](https://spec.commonmark.org/0.30/#link-reference-definitions)."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_trailing_link_flagged_in_strict() {
let rule = MD013LineLength::new(80, false, false, false, true);
let content = r#"For more information, see the [CommonMark specification](https://spec.commonmark.org/0.30/#link-reference-definitions)."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_warning_reports_actual_length() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "This line has a URL https://example.com/long/url and trailing text here";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(
result[0].message.contains("71"),
"Expected actual length 71 in message: {}",
result[0].message
);
}
#[test]
fn test_issue_393_list_item_with_link_chain() {
let rule = MD013LineLength::new(99, false, false, false, false);
let content =
"- [@kevinsuttle](https://kevinsuttle.com/)/[macOS-Defaults](https://github.com/kevinSuttle/macOS-Defaults)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_single_long_token_no_spaces() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "ThisIsASingleVeryLongTokenWithNoSpacesAtAllThatExceedsLimit";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_single_long_token_in_strict_mode() {
let rule = MD013LineLength::new(50, false, false, false, true); let content = "ThisIsASingleVeryLongTokenWithNoSpacesAtAllThatExceedsLimit";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_list_item_with_single_long_token() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "- ThisIsAVeryLongListItemTokenThatExceedsTheLimitButCannotBeBroken";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_trailing_url_forgiven() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "short text https://github.com/kevinSuttle/macOS-Defaults/really/long/path";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_trailing_url_flagged_when_prefix_exceeds_limit() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "This text is already very long before the URL even starts here https://example.com";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_bold_link_forgiven() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "**[Bold link text](https://github.com/kevinSuttle/macOS-Defaults/really/long/path)**";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_links_with_text_between_suppressed_when_text_short() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content =
"See [Link One](https://example.com/long/path) and also [Link Two](https://example.com/long/path) here";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_blockquote_ending_with_url_forgiven() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "> See https://github.com/kevinSuttle/macOS-Defaults/really/long/path";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_blockquote_with_wrappable_text_flagged() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "> This is a very long blockquote line with lots of wrappable text that exceeds the limit easily";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_link_ref_definition_exempt_in_strict_mode() {
let rule = MD013LineLength::new(50, false, false, false, true); let content = "[reference]: https://example.com/very/long/url/that/exceeds/the/configured/limit";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_link_ref_definition_exempt_in_non_strict_mode() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "[reference]: https://example.com/very/long/url/that/exceeds/the/configured/limit";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_link_ref_definition_with_double_quoted_title_exempt() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = r#"[polars.expr.qcut]: https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.Expr.qcut.html "Bin continuous values into discrete categories based on their quantiles.""#;
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link ref definition with double-quoted title should be exempt, got: {result:?}"
);
}
#[test]
fn test_link_ref_definition_with_single_quoted_title_exempt() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "[reference]: https://example.com/very/long/url/that/exceeds/the/configured/limit 'A single-quoted title that makes the line even longer'";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link ref definition with single-quoted title should be exempt, got: {result:?}"
);
}
#[test]
fn test_link_ref_definition_with_parenthesized_title_exempt() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "[reference]: https://example.com/very/long/url/that/exceeds/the/configured/limit (A parenthesized title that makes the line even longer)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link ref definition with parenthesized title should be exempt, got: {result:?}"
);
}
#[test]
fn test_link_ref_definition_non_http_url_exempt() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "[reference]: /very/long/relative/path/to/some/document/that/exceeds/the/limit/by/far.md";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link ref definition with non-HTTP URL should be exempt, got: {result:?}"
);
}
#[test]
fn test_link_ref_definition_non_http_url_with_title_exempt() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content =
"[reference]: /very/long/relative/path/to/some/document/that/exceeds.md \"A long title for the reference\"";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link ref definition with non-HTTP URL and title should be exempt, got: {result:?}"
);
}
#[test]
fn test_link_ref_definition_angle_bracket_url_exempt() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "[reference]: <https://example.com/very/long/url/that/exceeds/the/configured/limit>";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link ref definition with angle-bracket URL should be exempt, got: {result:?}"
);
}
#[test]
fn test_link_ref_definition_with_title_in_list_item_exempt() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = r#"- [polars.expr.qcut]: https://docs.pola.rs/api/python/stable/reference/api/polars.Expr.qcut.html "Bin continuous values""#;
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link ref definition with title inside list item should be exempt, got: {result:?}"
);
}
#[test]
fn test_link_ref_definition_with_title_exempt_in_strict_mode() {
let rule = MD013LineLength::new(50, false, false, false, true); let content = r#"[reference]: https://example.com/very/long/url/that/exceeds/the/configured/limit "Title""#;
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link ref definition with title should be exempt even in strict mode, got: {result:?}"
);
}
#[test]
fn test_link_ref_definition_no_space_after_colon_exempt() {
let rule = MD013LineLength::new(50, false, false, false, true); let content = "[reference]:https://example.com/very/long/url/that/exceeds/the/configured/limit";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link ref definition with no space after colon should be exempt, got: {result:?}"
);
}
#[test]
fn test_bracket_colon_non_link_ref_not_exempt() {
let rule = MD013LineLength::new(50, false, false, false, true); let content = "[WARNING]: Do not use this deprecated API in production code or any other environment because it will cause severe data loss";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Non-link-ref-def text starting with [WORD]: should NOT be exempt from MD013"
);
}
#[test]
fn test_trailing_word_replacement_preserves_warning_length() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "This line is already very long before the trailing https://example.com/long/url/path";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(
result[0].message.contains("84"),
"Expected actual length in message: {}",
result[0].message
);
}
#[test]
fn test_image_ref_without_spaces_forgiven() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "![very-long-image-alt-text-that-exceeds-the-line-limit-by-a-lot][ref]";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_markdownlint_documentation_examples() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "This line is okay because there are-no-spaces-beyond-that-length";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
assert_eq!(
rule.check(&ctx).unwrap().len(),
0,
"should pass: no spaces beyond limit"
);
let content = "This line is a violation because there are spaces beyond that length";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
assert_eq!(rule.check(&ctx).unwrap().len(), 1, "should flag: spaces beyond limit");
let content = "This-line-is-okay-because-there-are-no-spaces-anywhere-within";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
assert_eq!(rule.check(&ctx).unwrap().len(), 0, "should pass: no spaces anywhere");
}
#[test]
fn test_issue_384_reflow_with_urls() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(120),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- Use [`pre-commit`](https://pre-commit.com) (with [`prek`](https://prek.j178.dev)) to format and lint code. to format and lint code.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should flag: 133 chars > 120");
let fixed = rule.fix(&ctx).unwrap();
for line in fixed.lines() {
let len = line.chars().count();
assert!(len <= 120, "Line still too long after reflow: {line} ({len} chars)");
}
}
#[test]
fn test_text_reflow_simple() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(30),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is a very long line that definitely exceeds thirty characters and needs to be wrapped.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
for line in fixed.lines() {
assert!(
line.chars().count() <= 30,
"Line too long: {} (len={})",
line,
line.chars().count()
);
}
let fixed_words: Vec<&str> = fixed.split_whitespace().collect();
let original_words: Vec<&str> = content.split_whitespace().collect();
assert_eq!(fixed_words, original_words);
}
#[test]
fn test_text_reflow_preserves_markdown_elements() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(40),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This paragraph has **bold text** and *italic text* and [a link](https://example.com) that should be preserved.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("**bold text**"), "Bold text not preserved in: {fixed}");
assert!(fixed.contains("*italic text*"), "Italic text not preserved in: {fixed}");
assert!(
fixed.contains("[a link](https://example.com)"),
"Link not preserved in: {fixed}"
);
for line in fixed.lines() {
assert!(line.len() <= 40, "Line too long: {line}");
}
}
#[test]
fn test_text_reflow_preserves_code_blocks() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(30),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = r#"Here is some text.
```python
def very_long_function_name_that_exceeds_limit():
return "This should not be wrapped"
```
More text after code block."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("def very_long_function_name_that_exceeds_limit():"));
assert!(fixed.contains("```python"));
assert!(fixed.contains("```"));
}
#[test]
fn test_text_reflow_preserves_lists() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(30),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = r#"Here is a list:
1. First item with a very long line that needs wrapping
2. Second item is short
3. Third item also has a long line that exceeds the limit
And a bullet list:
- Bullet item with very long content that needs wrapping
- Short bullet"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("1. "));
assert!(fixed.contains("2. "));
assert!(fixed.contains("3. "));
assert!(fixed.contains("- "));
let lines: Vec<&str> = fixed.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.trim().starts_with("1.") || line.trim().starts_with("2.") || line.trim().starts_with("3.") {
if i + 1 < lines.len()
&& !lines[i + 1].trim().is_empty()
&& !lines[i + 1].trim().starts_with(char::is_numeric)
&& !lines[i + 1].trim().starts_with("-")
{
assert!(lines[i + 1].starts_with(" ") || lines[i + 1].trim().is_empty());
}
} else if line.trim().starts_with("-") {
if i + 1 < lines.len()
&& !lines[i + 1].trim().is_empty()
&& !lines[i + 1].trim().starts_with(char::is_numeric)
&& !lines[i + 1].trim().starts_with("-")
{
assert!(lines[i + 1].starts_with(" ") || lines[i + 1].trim().is_empty());
}
}
}
}
#[test]
fn test_issue_83_numbered_list_with_backticks() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. List `manifest` to find the manifest with the largest ID. Say it's `00000000000000000002.manifest` in this example.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = "1. List `manifest` to find the manifest with the largest ID. Say it's\n `00000000000000000002.manifest` in this example.";
assert_eq!(
fixed, expected,
"List should be properly reflowed with correct marker and indentation.\nExpected:\n{expected}\nGot:\n{fixed}"
);
}
#[test]
fn test_text_reflow_disabled_by_default() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "This is a very long line that definitely exceeds thirty characters.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content);
}
#[test]
fn test_reflow_with_hard_line_breaks() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(40),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This line has a hard break at the end \nAnd this continues on the next line that is also quite long and needs wrapping";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains(" \n"),
"Hard line break with exactly 2 spaces should be preserved"
);
}
#[test]
fn test_reflow_preserves_reference_links() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(40),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content =
"This is a very long line with a [reference link][ref] that should not be broken apart when reflowing the text.
[ref]: https://example.com";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("[reference link][ref]"));
assert!(!fixed.contains("[ reference link]"));
assert!(!fixed.contains("[ref ]"));
}
#[test]
fn test_reflow_with_nested_markdown_elements() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(35),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This text has **bold with `code` inside** and should handle it properly when wrapping";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("**bold with `code` inside**"));
}
#[test]
fn test_reflow_with_unbalanced_markdown() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(30),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This has **unbalanced bold that goes on for a very long time without closing";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(!fixed.is_empty());
for line in fixed.lines() {
assert!(line.len() <= 30 || line.starts_with("**"), "Line exceeds limit: {line}");
}
}
#[test]
fn test_reflow_italic_paragraph() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "# Lorem\n\n*Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed quam leo, rhoncus sodales erat sed. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed quam leo, rhoncus sodales erat sed.*\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
for line in fixed.lines() {
assert!(
line.len() <= 80,
"Line still exceeds limit after reflow: {:?} ({} chars)",
line,
line.len()
);
}
assert!(fixed.contains('*'), "Italic markers lost after reflow: {fixed}");
}
#[test]
fn test_reflow_bold_paragraph() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "**Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed quam leo, rhoncus sodales erat sed. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed quam leo, rhoncus sodales erat sed.**\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
for line in fixed.lines() {
assert!(
line.len() <= 80,
"Line still exceeds limit after reflow: {:?} ({} chars)",
line,
line.len()
);
}
assert!(fixed.contains("**"), "Bold markers lost after reflow: {fixed}");
}
#[test]
fn test_reflow_underscore_italic_paragraph() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(40),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "_Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed quam leo rhoncus._\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
for line in fixed.lines() {
assert!(
line.len() <= 40,
"Line still exceeds limit after reflow: {:?} ({} chars)",
line,
line.len()
);
}
assert!(
fixed.contains('_'),
"Underscore italic markers lost after reflow: {fixed}"
);
}
#[test]
fn test_reflow_inline_italic_not_broken() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(60),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This paragraph has some *italic text* that should stay intact.\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("*italic text*"), "Short inline italic broken: {fixed}");
}
#[test]
fn test_reflow_fix_indicator() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(30),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is a very long line that definitely exceeds the thirty character limit";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(!warnings.is_empty());
assert!(
warnings[0].fix.is_some(),
"Should provide fix indicator when reflow is true"
);
}
#[test]
fn test_no_fix_indicator_without_reflow() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(30),
reflow: false,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is a very long line that definitely exceeds the thirty character limit";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(!warnings.is_empty());
assert!(warnings[0].fix.is_none(), "Should not provide fix when reflow is false");
}
#[test]
fn test_reflow_preserves_all_reference_link_types() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(40),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "Test [full reference][ref] and [collapsed][] and [shortcut] reference links in a very long line.
[ref]: https://example.com
[collapsed]: https://example.com
[shortcut]: https://example.com";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("[full reference][ref]"));
assert!(fixed.contains("[collapsed][]"));
assert!(fixed.contains("[shortcut]"));
}
#[test]
fn test_reflow_handles_images_correctly() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(40),
reflow: true,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content =
"This line has an  that should not be broken when reflowing.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains(""));
}
#[test]
fn test_normalize_mode_flags_short_lines() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is a short line.\nAnother short line.\nA third short line that could be combined.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(!warnings.is_empty(), "Should flag paragraph for normalization");
assert!(warnings[0].message.contains("normalized"));
}
#[test]
fn test_normalize_mode_combines_short_lines() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content =
"This is a line with\nmanual line breaks at\n80 characters that should\nbe combined into longer lines.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 1, "Should combine into single line");
assert!(lines[0].len() > 80, "Should use more of the 100 char limit");
}
#[test]
fn test_normalize_mode_preserves_paragraph_breaks() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "First paragraph with\nshort lines.\n\nSecond paragraph with\nshort lines too.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("\n\n"), "Should preserve paragraph breaks");
let paragraphs: Vec<&str> = fixed.split("\n\n").collect();
assert_eq!(paragraphs.len(), 2, "Should have two paragraphs");
}
#[test]
fn test_default_mode_only_fixes_violations() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Default, ..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is a short line.\nAnother short line.\nA third short line.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(warnings.is_empty(), "Should not flag short lines in default mode");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed.lines().count(), 3, "Should preserve line breaks in default mode");
}
#[test]
fn test_normalize_mode_with_lists() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = r#"A paragraph with
short lines.
1. List item with
short lines
2. Another item"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert!(lines[0].len() > 20, "First paragraph should be normalized");
assert!(fixed.contains("1. "), "Should preserve list markers");
assert!(fixed.contains("2. "), "Should preserve list markers");
}
#[test]
fn test_normalize_mode_with_code_blocks() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = r#"A paragraph with
short lines.
```
code block should not be normalized
even with short lines
```
Another paragraph with
short lines."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("code block should not be normalized\neven with short lines"));
let lines: Vec<&str> = fixed.lines().collect();
assert!(lines[0].len() > 20, "First paragraph should be normalized");
}
#[test]
fn test_issue_76_use_case() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(999999), reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "We've decided to eliminate line-breaks in paragraphs. The obvious solution is\nto disable MD013, and call it good. However, that doesn't deal with the\nexisting content's line-breaks. My initial thought was to set line_length to\n999999 and enable_reflow, but realised after doing so, that it never triggers\nthe error, so nothing happens.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(!warnings.is_empty(), "Should flag paragraph for normalization");
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(lines.len(), 1, "Should combine into single line with high limit");
assert!(!fixed.contains("\n"), "Should remove all line breaks within paragraph");
}
#[test]
fn test_normalize_mode_single_line_unchanged() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is a single line that should not be changed.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(warnings.is_empty(), "Single line should not be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "Single line should remain unchanged");
}
#[test]
fn test_normalize_mode_with_inline_code() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content =
"This paragraph has `inline code` and\nshould still be normalized properly\nwithout breaking the code.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(!warnings.is_empty(), "Multi-line paragraph should be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("`inline code`"), "Inline code should be preserved");
assert!(fixed.lines().count() < 3, "Lines should be combined");
}
#[test]
fn test_normalize_mode_with_emphasis() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This has **bold** and\n*italic* text that\nshould be preserved.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("**bold**"), "Bold should be preserved");
assert!(fixed.contains("*italic*"), "Italic should be preserved");
assert_eq!(fixed.lines().count(), 1, "Should be combined into one line");
}
#[test]
fn test_normalize_mode_respects_hard_breaks() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "First line with hard break \nSecond line after break\nThird line";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains(" \n"), "Hard break should be preserved");
assert!(
fixed.contains("Second line after break Third line"),
"Lines without hard break should combine"
);
}
#[test]
fn test_normalize_mode_with_links() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This has a [link](https://example.com) that\nshould be preserved when\nnormalizing the paragraph.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("[link](https://example.com)"),
"Link should be preserved"
);
assert_eq!(fixed.lines().count(), 1, "Should be combined into one line");
}
#[test]
fn test_normalize_mode_empty_lines_between_paragraphs() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "First paragraph\nwith multiple lines.\n\n\nSecond paragraph\nwith multiple lines.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("\n\n\n"), "Multiple empty lines should be preserved");
let parts: Vec<&str> = fixed.split("\n\n\n").collect();
assert_eq!(parts.len(), 2, "Should have two parts");
assert_eq!(parts[0].lines().count(), 1, "First paragraph should be one line");
assert_eq!(parts[1].lines().count(), 1, "Second paragraph should be one line");
}
#[test]
fn test_normalize_mode_mixed_list_types() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = r#"Paragraph before list
with multiple lines.
- Bullet item
* Another bullet
+ Plus bullet
1. Numbered item
2. Another number
Paragraph after list
with multiple lines."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("- Bullet item"), "Dash list should be preserved");
assert!(fixed.contains("* Another bullet"), "Star list should be preserved");
assert!(fixed.contains("+ Plus bullet"), "Plus list should be preserved");
assert!(fixed.contains("1. Numbered item"), "Numbered list should be preserved");
assert!(
fixed.starts_with("Paragraph before list with multiple lines."),
"First paragraph should be normalized"
);
assert!(
fixed.ends_with("Paragraph after list with multiple lines."),
"Last paragraph should be normalized"
);
}
#[test]
fn test_normalize_mode_with_horizontal_rules() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "Paragraph before\nhorizontal rule.\n\n---\n\nParagraph after\nhorizontal rule.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("---"), "Horizontal rule should be preserved");
assert!(
fixed.contains("Paragraph before horizontal rule."),
"First paragraph normalized"
);
assert!(
fixed.contains("Paragraph after horizontal rule."),
"Second paragraph normalized"
);
}
#[test]
fn test_normalize_mode_with_indented_code() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "Paragraph before\nindented code.\n\n This is indented code\n Should not be normalized\n\nParagraph after\nindented code.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains(" This is indented code\n Should not be normalized"),
"Indented code preserved"
);
assert!(
fixed.contains("Paragraph before indented code."),
"First paragraph normalized"
);
assert!(
fixed.contains("Paragraph after indented code."),
"Second paragraph normalized"
);
}
#[test]
fn test_normalize_mode_disabled_without_reflow() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: false, reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is a line\nwith breaks that\nshould not be changed.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert!(warnings.is_empty(), "Should not flag when reflow is disabled");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "Content should be unchanged when reflow is disabled");
}
#[test]
fn test_default_mode_with_long_lines() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(50),
reflow: true,
reflow_mode: ReflowMode::Default,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "Short line.\nThis is a very long line that definitely exceeds the fifty character limit and needs wrapping.\nAnother short line.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 1, "Should flag the paragraph with long line");
assert_eq!(warnings[0].line, 2, "Should flag line 2 that exceeds limit");
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("Short line. This is"),
"Should combine and reflow the paragraph"
);
assert!(
fixed.contains("wrapping. Another short"),
"Should include all paragraph content"
);
}
#[test]
fn test_normalize_vs_default_mode_same_content() {
let content = "This is a paragraph\nwith multiple lines\nthat could be combined.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let default_config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Default,
..Default::default()
};
let default_rule = MD013LineLength::from_config_struct(default_config);
let default_warnings = default_rule.check(&ctx).unwrap();
let default_fixed = default_rule.fix(&ctx).unwrap();
let normalize_config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let normalize_rule = MD013LineLength::from_config_struct(normalize_config);
let normalize_warnings = normalize_rule.check(&ctx).unwrap();
let normalize_fixed = normalize_rule.fix(&ctx).unwrap();
assert!(default_warnings.is_empty(), "Default mode should not flag short lines");
assert!(
!normalize_warnings.is_empty(),
"Normalize mode should flag multi-line paragraphs"
);
assert_eq!(
default_fixed, content,
"Default mode should not change content without violations"
);
assert_ne!(
normalize_fixed, content,
"Normalize mode should change multi-line paragraphs"
);
assert_eq!(
normalize_fixed.lines().count(),
1,
"Normalize should combine into single line"
);
}
#[test]
fn test_normalize_mode_with_reference_definitions() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This paragraph uses\na reference [link][ref]\nacross multiple lines.\n\n[ref]: https://example.com";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("[link][ref]"), "Reference link should be preserved");
assert!(
fixed.contains("[ref]: https://example.com"),
"Reference definition should be preserved"
);
assert!(
fixed.starts_with("This paragraph uses a reference [link][ref] across multiple lines."),
"Paragraph should be normalized"
);
}
#[test]
fn test_normalize_mode_with_html_comments() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "Paragraph before\nHTML comment.\n\n<!-- This is a comment -->\n\nParagraph after\nHTML comment.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("<!-- This is a comment -->"),
"HTML comment should be preserved"
);
assert!(
fixed.contains("Paragraph before HTML comment."),
"First paragraph normalized"
);
assert!(
fixed.contains("Paragraph after HTML comment."),
"Second paragraph normalized"
);
}
#[test]
fn test_normalize_mode_line_starting_with_number() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This line mentions\n80 characters which\nshould not break the paragraph.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed.lines().count(), 1, "Should be combined into single line");
assert!(
fixed.contains("80 characters"),
"Number at start of line should be preserved"
);
}
#[test]
fn test_default_mode_preserves_list_structure() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
reflow: true,
reflow_mode: ReflowMode::Default,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = r#"- This is a bullet point that has
some text on multiple lines
that should stay separate
1. Numbered list item with
multiple lines that should
also stay separate"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let lines: Vec<&str> = fixed.lines().collect();
assert_eq!(
lines[0], "- This is a bullet point that has",
"First line should be unchanged"
);
assert_eq!(
lines[1], " some text on multiple lines",
"Continuation should be preserved"
);
assert_eq!(
lines[2], " that should stay separate",
"Second continuation should be preserved"
);
}
#[test]
fn test_normalize_mode_multi_line_list_items_no_extra_spaces() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = r#"- This is a bullet point that has
some text on multiple lines
that should be combined
1. Numbered list item with
multiple lines that need
to be properly combined
2. Second item"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
!fixed.contains("lines that"),
"Should not have double spaces in bullet list"
);
assert!(
!fixed.contains("need to"),
"Should not have double spaces in numbered list"
);
assert!(
fixed.contains("- This is a bullet point that has some text on multiple lines that should be"),
"Bullet list should be properly combined"
);
assert!(
fixed.contains("1. Numbered list item with multiple lines that need to be properly combined"),
"Numbered list should be properly combined"
);
}
#[test]
fn test_normalize_mode_actual_numbered_list() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(100),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "Paragraph before list\nwith multiple lines.\n\n1. First item\n2. Second item\n10. Tenth item";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("1. First item"), "Numbered list 1 should be preserved");
assert!(fixed.contains("2. Second item"), "Numbered list 2 should be preserved");
assert!(fixed.contains("10. Tenth item"), "Numbered list 10 should be preserved");
assert!(
fixed.starts_with("Paragraph before list with multiple lines."),
"Paragraph should be normalized"
);
}
#[test]
fn test_sentence_per_line_detection() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::SentencePerLine,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config.clone());
let content = "This is sentence one. This is sentence two. And sentence three!";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
assert!(!rule.should_skip(&ctx), "Should not skip for sentence-per-line mode");
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect multiple sentences on one line");
assert_eq!(
result[0].message,
"Line contains 3 sentences (one sentence per line required)"
);
}
#[test]
fn test_sentence_per_line_fix() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::SentencePerLine,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "First sentence. Second sentence.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect violation");
assert!(result[0].fix.is_some(), "Should provide a fix");
let fix = result[0].fix.as_ref().unwrap();
assert_eq!(fix.replacement.trim(), "First sentence.\nSecond sentence.");
}
#[test]
fn test_sentence_per_line_abbreviations() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::SentencePerLine,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "Mr. Smith met Dr. Jones at 3:00 PM.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not detect abbreviations as sentence boundaries"
);
}
#[test]
fn test_sentence_per_line_with_markdown() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::SentencePerLine,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "# Heading\n\nSentence with **bold**. Another with [link](url).";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect multiple sentences with markdown");
assert_eq!(result[0].line, 3); }
#[test]
fn test_sentence_per_line_questions_exclamations() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::SentencePerLine,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "Is this a question? Yes it is! And a statement.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect sentences with ? and !");
let fix = result[0].fix.as_ref().unwrap();
let lines: Vec<&str> = fix.replacement.trim().lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "Is this a question?");
assert_eq!(lines[1], "Yes it is!");
assert_eq!(lines[2], "And a statement.");
}
#[test]
fn test_sentence_per_line_in_lists() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::SentencePerLine,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- List item one. With two sentences.\n- Another item.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect sentences in list items");
let fix = result[0].fix.as_ref().unwrap();
assert!(fix.replacement.starts_with("- "), "Should preserve list marker");
}
#[test]
fn test_multi_paragraph_list_item_with_3_space_indent() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(999999),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. First paragraph\n continuation line.\n\n Second paragraph\n more content.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect multi-line paragraphs in list item");
let fix = result[0].fix.as_ref().unwrap();
assert!(
fix.replacement.contains("\n\n"),
"Should preserve blank line between paragraphs"
);
assert!(fix.replacement.starts_with("1. "), "Should preserve list marker");
}
#[test]
fn test_multi_paragraph_list_item_with_4_space_indent() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(999999),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. It **generated an application template**. There's a lot of files and\n configurations required to build a native installer, above and\n beyond the code of your actual application.\n\n If you're not happy with the template provided by Briefcase, you can\n provide your own.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Should detect multi-line paragraphs in list item with 4-space indent"
);
let fix = result[0].fix.as_ref().unwrap();
assert!(
fix.replacement.contains("\n\n"),
"Should preserve blank line between paragraphs"
);
assert!(fix.replacement.starts_with("1. "), "Should preserve list marker");
let lines: Vec<&str> = fix.replacement.split('\n').collect();
let blank_line_idx = lines.iter().position(|l| l.trim().is_empty());
assert!(blank_line_idx.is_some(), "Should have blank line separating paragraphs");
}
#[test]
fn test_multi_paragraph_bullet_list_item() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(999999),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- First paragraph\n continuation.\n\n Second paragraph\n more text.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect multi-line paragraphs in bullet list");
let fix = result[0].fix.as_ref().unwrap();
assert!(
fix.replacement.contains("\n\n"),
"Should preserve blank line between paragraphs"
);
assert!(fix.replacement.starts_with("- "), "Should preserve bullet marker");
}
#[test]
fn test_code_block_in_list_item_five_spaces() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. First paragraph with some text that should be reflowed.\n\n code_block()\n more_code()\n\n Second paragraph.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
if !result.is_empty() {
let fix = result[0].fix.as_ref().unwrap();
assert!(
fix.replacement.contains(" code_block()"),
"Code block should be preserved: {}",
fix.replacement
);
assert!(
fix.replacement.contains(" more_code()"),
"Code block should be preserved: {}",
fix.replacement
);
}
}
#[test]
fn test_fenced_code_block_in_list_item() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. First paragraph with some text.\n\n ```rust\n fn foo() {}\n let x = 1;\n ```\n\n Second paragraph.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
if !result.is_empty() {
let fix = result[0].fix.as_ref().unwrap();
assert!(
fix.replacement.contains("```rust"),
"Should preserve fence: {}",
fix.replacement
);
assert!(
fix.replacement.contains("fn foo() {}"),
"Should preserve code: {}",
fix.replacement
);
assert!(
fix.replacement.contains("```"),
"Should preserve closing fence: {}",
fix.replacement
);
}
}
#[test]
fn test_mixed_indentation_3_and_4_spaces() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(999999),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. Text\n 3 space continuation\n 4 space continuation";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect multi-line list item");
let fix = result[0].fix.as_ref().unwrap();
assert!(
fix.replacement.contains("3 space continuation"),
"Should include 3-space line: {}",
fix.replacement
);
assert!(
fix.replacement.contains("4 space continuation"),
"Should include 4-space line: {}",
fix.replacement
);
}
#[test]
fn test_nested_list_in_multi_paragraph_item() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(999999),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. First paragraph.\n\n - Nested item\n continuation\n\n Second paragraph.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
let parent_warnings: Vec<_> = result.iter().filter(|w| w.line == 1).collect();
assert!(
parent_warnings.is_empty(),
"Parent single-line item should not produce a warning: {:?}",
parent_warnings.iter().map(|w| (&w.message, w.line)).collect::<Vec<_>>()
);
let nested_warnings: Vec<_> = result.iter().filter(|w| w.line == 3).collect();
if !nested_warnings.is_empty() {
let fix = nested_warnings[0].fix.as_ref().unwrap();
assert!(
fix.replacement.contains("Nested item"),
"Nested fix should contain nested content: {}",
fix.replacement
);
}
}
#[test]
fn test_nested_fence_markers_different_types() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. Example with nested fences:\n\n ~~~markdown\n This shows ```python\n code = True\n ```\n ~~~\n\n Text after.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
if !result.is_empty() {
let fix = result[0].fix.as_ref().unwrap();
assert!(
fix.replacement.contains("```python"),
"Should preserve inner fence: {}",
fix.replacement
);
assert!(
fix.replacement.contains("~~~"),
"Should preserve outer fence: {}",
fix.replacement
);
assert!(
fix.replacement.contains("code = True"),
"Should preserve code: {}",
fix.replacement
);
}
}
#[test]
fn test_nested_fence_markers_same_type() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content =
"1. Example:\n\n ````markdown\n Shows ```python in code\n ```\n text here\n ````\n\n After.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
if !result.is_empty() {
let fix = result[0].fix.as_ref().unwrap();
assert!(
fix.replacement.contains("```python"),
"Should preserve inner fence: {}",
fix.replacement
);
assert!(
fix.replacement.contains("````"),
"Should preserve outer fence: {}",
fix.replacement
);
assert!(
fix.replacement.contains("text here"),
"Should keep text as code: {}",
fix.replacement
);
}
}
#[test]
fn test_sibling_list_item_breaks_parent() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(999999),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. First item\n continuation.\n2. Second item";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
if !result.is_empty() {
let fix = result[0].fix.as_ref().unwrap();
assert!(fix.replacement.starts_with("1. "), "Should start with first marker");
assert!(fix.replacement.contains("continuation"), "Should include continuation");
}
}
#[test]
fn test_nested_list_at_continuation_indent_preserved() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(999999),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. Parent paragraph\n with continuation.\n\n - Nested at 3 spaces\n - Another nested\n\n After nested.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
if !result.is_empty() {
let fix = result[0].fix.as_ref().unwrap();
assert!(
fix.replacement.contains("Parent paragraph with continuation."),
"Parent content should be merged: {}",
fix.replacement
);
assert!(
!fix.replacement.contains("- Nested"),
"Nested items should not be in parent fix (they are processed independently): {}",
fix.replacement
);
}
}
#[test]
fn test_paragraphs_false_skips_regular_text() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(50),
paragraphs: false, code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: false,
reflow_mode: ReflowMode::default(),
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content =
"This is a very long line of regular text that exceeds fifty characters and should not trigger a warning.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Should not warn about long paragraph text when paragraphs=false"
);
}
#[test]
fn test_paragraphs_false_still_checks_code_blocks() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(50),
paragraphs: false, code_blocks: true, tables: true,
headings: true,
strict: false,
reflow: false,
reflow_mode: ReflowMode::default(),
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = r#"```
This is a very long line in a code block that exceeds fifty characters.
```"#;
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Should warn about long lines in code blocks even when paragraphs=false"
);
}
#[test]
fn test_paragraphs_false_still_checks_headings() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(50),
paragraphs: false, code_blocks: true,
tables: true,
headings: true, strict: false,
reflow: false,
reflow_mode: ReflowMode::default(),
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = "# This is a very long heading that exceeds fifty characters and should trigger a warning";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Should warn about long headings even when paragraphs=false"
);
}
#[test]
fn test_paragraphs_false_with_reflow_sentence_per_line() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: false,
code_blocks: true,
tables: true,
headings: false,
strict: false,
reflow: true,
reflow_mode: ReflowMode::SentencePerLine,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is a very long sentence that exceeds eighty characters and contains important information that should not be flagged.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Should not warn about long sentences when paragraphs=false"
);
}
#[test]
fn test_paragraphs_true_checks_regular_text() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(50),
paragraphs: true, code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: false,
reflow_mode: ReflowMode::default(),
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is a very long line of regular text that exceeds fifty characters.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Should warn about long paragraph text when paragraphs=true"
);
}
#[test]
fn test_line_length_zero_disables_all_checks() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(0), paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: false,
reflow_mode: ReflowMode::default(),
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is a very very very very very very very very very very very very very very very very very very very very very very very very long line that would normally trigger MD013.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Should not warn about any line length when line_length = 0"
);
}
#[test]
fn test_line_length_zero_with_headings() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(0), paragraphs: true,
code_blocks: true,
tables: true,
headings: true, strict: false,
reflow: false,
reflow_mode: ReflowMode::default(),
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = "# This is a very very very very very very very very very very very very very very very very very very very very very long heading";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Should not warn about heading line length when line_length = 0"
);
}
#[test]
fn test_line_length_zero_with_code_blocks() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(0), paragraphs: true,
code_blocks: true, tables: true,
headings: true,
strict: false,
reflow: false,
reflow_mode: ReflowMode::default(),
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = "```\nThis is a very very very very very very very very very very very very very very very very very very very very very long code line\n```";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Should not warn about code block line length when line_length = 0"
);
}
#[test]
fn test_line_length_zero_with_sentence_per_line_reflow() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(0), paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::SentencePerLine,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is sentence one. This is sentence two. This is sentence three.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should provide reflow fix for multiple sentences");
assert!(result[0].fix.is_some(), "Should have a fix available");
}
#[test]
fn test_line_length_zero_config_parsing() {
let toml_str = r#"
line-length = 0
paragraphs = true
reflow = true
reflow-mode = "sentence-per-line"
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.line_length.get(), 0, "Should parse line_length = 0");
assert!(config.line_length.is_unlimited(), "Should be unlimited");
assert!(config.paragraphs);
assert!(config.reflow);
assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
}
#[test]
fn test_template_directives_as_paragraph_boundaries() {
let content = r#"Some regular text here.
{{#tabs }}
{{#tab name="Tab 1" }}
More text in the tab.
{{#endtab }}
{{#tabs }}
Final paragraph.
"#;
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
code_blocks: true,
tables: true,
headings: true,
paragraphs: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::SentencePerLine,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let result = rule.check(&ctx).unwrap();
for warning in &result {
assert!(
!warning.message.contains("multiple sentences"),
"Template directives should not trigger 'multiple sentences' warning. Got: {}",
warning.message
);
}
}
#[test]
fn test_template_directive_detection() {
assert!(is_template_directive_only("{{#tabs }}"));
assert!(is_template_directive_only("{{#endtab }}"));
assert!(is_template_directive_only("{{variable}}"));
assert!(is_template_directive_only(" {{#tabs }} "));
assert!(is_template_directive_only("{% for item in items %}"));
assert!(is_template_directive_only("{%endfor%}"));
assert!(is_template_directive_only(" {% if condition %} "));
assert!(!is_template_directive_only("This is {{variable}} in text"));
assert!(!is_template_directive_only("{{incomplete"));
assert!(!is_template_directive_only("incomplete}}"));
assert!(!is_template_directive_only(""));
assert!(!is_template_directive_only(" "));
assert!(!is_template_directive_only("Regular text"));
}
#[test]
fn test_mixed_content_with_templates() {
let content = "This has {{variable}} in the middle.";
assert!(!is_template_directive_only(content));
let content2 = "Start {{#something}} end";
assert!(!is_template_directive_only(content2));
}
#[test]
fn test_reflow_preserves_mkdocstrings_autodoc_block() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::SemanticLineBreaks,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = "::: path.to.module\n options:\n group_by_category: false\n members:\n";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let reflow_fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(
reflow_fixes.is_empty(),
"mkdocstrings autodoc blocks should not be reflowed, got {reflow_fixes:?}"
);
}
#[test]
fn test_reflow_preserves_mkdocstrings_with_identifier() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::SentencePerLine,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content =
"::: my_module.MyClass\n handler: python\n options:\n show_source: true\n heading_level: 3\n";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let reflow_fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(
reflow_fixes.is_empty(),
"mkdocstrings autodoc blocks should not produce reflow fixes, got {reflow_fixes:?}"
);
}
#[test]
fn test_reflow_preserves_mkdocstrings_surrounded_by_paragraphs() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(40),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::SemanticLineBreaks,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = "This is a long paragraph that exceeds the forty character line length limit.\n\n::: my_module.MyClass\n handler: python\n options:\n show_source: true\n\nAnother long paragraph that also exceeds the forty character line length limit.\n";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
for warning in &result {
if let Some(ref fix) = warning.fix {
let fixed = &fix.replacement;
assert!(
!fixed.contains("handler:") && !fixed.contains("show_source:"),
"mkdocstrings YAML options should not appear in reflow fixes: {fixed}"
);
}
}
}
#[test]
fn test_reflow_mkdocstrings_not_detected_in_standard_flavor() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::SemanticLineBreaks,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = "::: my_module.MyClass\n handler: python\n options:\n show_source: true\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let _result = rule.check(&ctx).unwrap();
}
#[test]
fn test_reflow_preserves_mkdocstrings_with_blank_line_in_block() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::SemanticLineBreaks,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = "::: path.to.module\n handler: python\n\n options:\n show_source: true\n";
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let reflow_fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(
reflow_fixes.is_empty(),
"mkdocstrings blocks with blank lines should not be reflowed, got {reflow_fixes:?}"
);
}
#[test]
fn test_semantic_link_basic_suppression() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "Click [here](https://example.com/very/long/path/to/resource/page) now.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should suppress warning when URL removal brings line within limit"
);
}
#[test]
fn test_semantic_link_text_still_too_long() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "This is very long text that exceeds the limit [link](https://example.com) more text here";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should warn when text alone exceeds limit");
}
#[test]
fn test_semantic_link_multiple_links() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "See [foo](https://example.com/foo/path) and [bar](https://example.com/bar/path) here.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should suppress when multiple links' URLs account for the excess"
);
}
#[test]
fn test_semantic_link_image_suppression() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "See  here.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should suppress when image URL accounts for the excess"
);
}
#[test]
fn test_semantic_link_reference_links_no_savings() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "This is a line with a [reference link][ref] that is quite long and exceeds the limit.\n\n[ref]: https://example.com";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
let line1_warnings: Vec<_> = result.iter().filter(|w| w.line == 1).collect();
assert!(!line1_warnings.is_empty(), "Reference links provide no URL savings");
}
#[test]
fn test_semantic_link_strict_mode_no_suppression() {
let rule = MD013LineLength::new(40, false, false, false, true);
let content = "Click [here](https://example.com/very/long/path/to/resource/page) now.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Strict mode should not suppress even when URL accounts for excess"
);
}
#[test]
fn test_semantic_link_with_title() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "Click [here](https://example.com/path \"A helpful title\") now.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should suppress when link with title URL accounts for excess"
);
}
#[test]
fn test_semantic_link_nested_badge() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content =
"Status [](https://ci.example.com/builds/latest)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should suppress for nested badge constructs");
}
#[test]
fn test_semantic_link_no_links_on_line() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "This is a very long line without any links that definitely exceeds thirty chars.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should warn when no links to strip");
}
#[test]
fn test_semantic_link_autolinks_no_savings() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "Visit <https://example.com/very/long/path/to/resource/page> for details.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let _result = rule.check(&ctx).unwrap();
}
#[test]
fn test_semantic_link_mixed_inline_and_reference() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "See [docs](https://example.com/long/docs/path) and [more][ref] for details and info.\n\n[ref]: https://example.com";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
let line1_warnings: Vec<_> = result.iter().filter(|w| w.line == 1).collect();
assert!(
line1_warnings.is_empty(),
"Inline link savings should bring line within limit"
);
}
#[test]
fn test_semantic_link_bold_text_in_link() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "Click [**important docs**](https://example.com/very/long/path/docs) now.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should handle markdown formatting inside link text");
}
#[test]
fn test_semantic_link_code_span_in_link() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "See [`Config`](https://example.com/long/api/Config) here.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should handle code spans inside link text");
}
#[test]
fn test_semantic_link_url_with_parentheses() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "See [article](https://en.wikipedia.org/wiki/Rust_(programming_language)) here.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should handle URLs with parentheses");
}
#[test]
fn test_semantic_link_only_partial_savings() {
let rule = MD013LineLength::new(50, false, false, false, false);
let content = "This is quite a long line of text with a short [link](https://x.co) and more text after it.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Should warn when savings aren't enough to bring under limit"
);
}
#[test]
fn test_semantic_link_boundary_exactly_at_limit() {
let rule = MD013LineLength::new(20, false, false, false, false);
let content = "abcdefghijklmnop [x](https://example.com/long/path)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should suppress when text-only length equals limit exactly"
);
}
#[test]
fn test_semantic_link_boundary_one_over_limit() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "abcdefghijklmnopqrstuvwxyz0123456789ab [x](https://a.co/1) [y](https://b.co/2)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should warn when text-only length exceeds limit");
}
#[test]
fn test_semantic_link_empty_link_text() {
let rule = MD013LineLength::new(20, false, false, false, false);
let content = "See [](https://example.com/very/long/path/to/resource) here.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should handle empty link text correctly");
}
#[test]
fn test_semantic_link_empty_image_alt() {
let rule = MD013LineLength::new(20, false, false, false, false);
let content = "See  here.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should handle empty image alt text correctly");
}
#[test]
fn test_semantic_link_entire_line_is_link() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "[documentation](https://example.com/very/long/path/to/documentation/page/section)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should suppress when entire line is a link with short text"
);
}
#[test]
fn test_semantic_link_in_blockquote() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "> See the [guide](https://example.com/very/long/path/to/guide) for details.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should suppress link URL excess in blockquotes");
}
#[test]
fn test_semantic_link_long_text_short_url() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "See the [very long descriptive link text that explains everything](https://x.co) here.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should warn when link text itself is long");
}
#[test]
fn test_semantic_link_multiple_images() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = " ";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should suppress when multiple image URLs account for excess"
);
}
#[test]
fn test_semantic_link_in_list_item() {
let rule = MD013LineLength::new(40, false, false, false, false);
let content = "- Click [here](https://example.com/very/long/path/to/resource/page) now.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should suppress link URL excess in list items");
}
#[test]
fn test_standalone_link_exempt_when_text_exceeds_limit() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "[some article with a very long title for demonstration](https://example.com/long-path)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Standalone link should be exempt even with long text"
);
}
#[test]
fn test_standalone_link_in_list_exempt() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "- [some article with a very long title for demonstration](https://example.com/path)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Standalone link in list item should be exempt");
}
#[test]
fn test_standalone_link_in_blockquote_exempt() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "> [some article with a very long title for demonstration](https://example.com/path)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Standalone link in blockquote should be exempt");
}
#[test]
fn test_standalone_image_exempt() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Standalone image should be exempt");
}
#[test]
fn test_standalone_link_with_emphasis_exempt() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "**[some article with a very long title for demonstration](https://example.com/path)**";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Bold standalone link should be exempt");
}
#[test]
fn test_standalone_link_not_exempt_in_strict_mode() {
let rule = MD013LineLength::new(30, false, false, false, true);
let content = "[some article with a very long title for demonstration](https://example.com/long-path)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Standalone link should NOT be exempt in strict mode"
);
}
#[test]
fn test_text_before_link_not_exempt() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "Some text before the actual link here [title](https://example.com)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Line with text before link should be flagged when text exceeds limit"
);
}
#[test]
fn test_standalone_reference_link_exempt() {
let rule = MD013LineLength::new(30, false, false, false, false);
let content = "[some article with a very long title for demonstration][ref1]\n\n[ref1]: https://example.com";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Standalone reference-style link should be exempt");
}
#[test]
fn test_blockquote_reflow_generates_fix_for_explicit_quote() {
let config = MD013Config {
line_length: crate::types::LineLength::new(40),
reflow: true,
reflow_mode: ReflowMode::Default,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "> This is a very long blockquote line that should be reflowed by MD013 when reflow is enabled.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].fix.is_some(), "Expected a blockquote reflow fix");
let fixed = rule.fix(&ctx).unwrap();
assert_ne!(fixed, content);
assert!(fixed.lines().all(|line| line.starts_with("> ")));
}
#[test]
fn test_blockquote_reflow_preserves_lazy_style() {
let config = MD013Config {
line_length: crate::types::LineLength::new(42),
reflow: true,
reflow_mode: ReflowMode::Default,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "> This opening quoted line is long enough that reflow must wrap it to multiple lines and preserve style.\nthis lazy continuation should remain lazy when safe to do so.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let fixed_lines: Vec<&str> = fixed.lines().collect();
assert!(!fixed_lines.is_empty());
assert!(fixed_lines[0].starts_with("> "));
assert!(
fixed_lines.iter().skip(1).any(|line| !line.starts_with('>')),
"Expected at least one lazy continuation line: {fixed}"
);
}
#[test]
fn test_blockquote_reflow_mixed_style_tie_resolves_explicit() {
let config = MD013Config {
line_length: crate::types::LineLength::new(44),
reflow: true,
reflow_mode: ReflowMode::Default,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "> This is an explicit quoted line that is intentionally long for wrapping behavior.\nlazy continuation text that participates in the same quote paragraph.\n> Another explicit continuation line to create a style tie for continuations.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let fixed_lines: Vec<&str> = fixed.lines().collect();
assert!(!fixed_lines.is_empty());
assert!(
fixed_lines.iter().all(|line| line.starts_with("> ")),
"Tie should resolve to explicit continuation style: {fixed}"
);
}
#[test]
fn test_blockquote_reflow_preserves_nested_prefix_style() {
let config = MD013Config {
line_length: crate::types::LineLength::new(40),
reflow: true,
reflow_mode: ReflowMode::Default,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "> > This nested quote paragraph is very long and should be wrapped while preserving the spaced nested prefix style.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.lines().all(|line| line.starts_with("> > ")),
"Expected spaced nested blockquote prefix to be preserved: {fixed}"
);
}
#[test]
fn test_blockquote_reflow_preserves_hard_break_markers() {
let config = MD013Config {
line_length: crate::types::LineLength::new(36),
reflow: true,
reflow_mode: ReflowMode::Default,
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "> This quoted line ends with a hard break marker and should keep it after wrapping.\\\nsecond sentence that should remain in the same quote paragraph and be wrapped.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.lines().any(|line| line.starts_with("> ") && line.ends_with('\\')),
"Expected hard break marker on a '> '-prefixed blockquote line: {fixed}"
);
let backslash_count = fixed.lines().filter(|l| l.ends_with('\\')).count();
assert_eq!(
backslash_count, 1,
"Expected exactly one hard break marker in output, got {backslash_count}: {fixed}"
);
let lines: Vec<&str> = fixed.lines().collect();
let marker_pos = lines.iter().position(|l| l.ends_with('\\')).unwrap();
for line in &lines[..marker_pos] {
assert!(
!line.ends_with('\\'),
"Found unexpected backslash before segment boundary in: {line:?}\nFull output: {fixed}"
);
}
}
#[test]
fn test_reflow_no_double_blanks_between_blocks() {
use crate::fix_coordinator::FixCoordinator;
use crate::rules::Rule;
use crate::rules::md013_line_length::MD013LineLength;
let content = "\
* `debug`: Enables you to set up a debugger. Currently, VS Code supports debugging Node.js and Python MCP servers.
<details>
<summary>Node.js MCP server</summary>
To debug a Node.js MCP server, set the property to `node`.
```json
{\"servers\": {}}
```
</details>
";
let rule: Box<dyn Rule> = Box::new(MD013LineLength::new(80, false, false, false, true));
let rules = vec![rule];
let mut fixed = content.to_string();
let coordinator = FixCoordinator::new();
coordinator
.apply_fixes_iterative(&rules, &[], &mut fixed, &Default::default(), 10, None)
.expect("fix should not fail");
let lines: Vec<&str> = fixed.lines().collect();
for i in 0..lines.len().saturating_sub(1) {
assert!(
!(lines[i].is_empty() && lines[i + 1].is_empty()),
"Double blank at lines {},{} in:\n{fixed}",
i + 1,
i + 2
);
}
let content2 = "\
1. Review the workflow configuration
1. Select **Models** > **Conversion** in the sidebar
The workflow will always execute the conversion step. This step cannot be disabled because it transforms the model.
";
let rule2: Box<dyn Rule> = Box::new(MD013LineLength::new(80, false, false, false, true));
let rules2 = vec![rule2];
let mut fixed2 = content2.to_string();
let coordinator2 = FixCoordinator::new();
coordinator2
.apply_fixes_iterative(&rules2, &[], &mut fixed2, &Default::default(), 10, None)
.expect("fix should not fail");
let lines2: Vec<&str> = fixed2.lines().collect();
for i in 0..lines2.len().saturating_sub(1) {
assert!(
!(lines2[i].is_empty() && lines2[i + 1].is_empty()),
"Double blank at lines {},{} in:\n{fixed2}",
i + 1,
i + 2
);
}
}
#[test]
fn test_issue_439_overindented_continuation_normalized() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed quam leo, rhoncus sodales erat sed. Lorem ipsum dolor sit amet, consectetur adipiscing\n elit. Sed quam leo, rhoncus sodales erat sed.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect line exceeding 80 chars");
let fix = result[0].fix.as_ref().expect("Should have a fix");
for line in fix.replacement.lines().skip(1) {
if !line.is_empty() {
assert!(
line.starts_with(" ") && !line.starts_with(" "),
"Continuation line should have exactly 2-space indent (marker_len), got: {line:?}"
);
}
}
let content2 = "1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed quam leo, rhoncus sodales erat sed. Lorem ipsum dolor sit amet, consectetur adipiscing\n elit. Sed quam leo, rhoncus sodales erat sed.";
let ctx2 = crate::lint_context::LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
let result2 = rule.check(&ctx2).unwrap();
assert!(!result2.is_empty(), "Should detect line exceeding 80 chars");
let fix2 = result2[0].fix.as_ref().expect("Should have a fix");
for line in fix2.replacement.lines().skip(1) {
if !line.is_empty() {
assert!(
line.starts_with(" ") && !line.starts_with(" "),
"Continuation line should have exactly 3-space indent (marker_len), got: {line:?}"
);
}
}
}
#[test]
fn test_overindented_continuation_all_list_types() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let cases = [
(
"- Item text that is long enough to be reflowed when reaching the limit here\n over-indented continuation",
2,
"bullet '- '",
),
(
"* Item text that is long enough to be reflowed when reaching the limit here\n over-indented continuation",
2,
"bullet '* '",
),
(
"+ Item text that is long enough to be reflowed when reaching the limit here\n over-indented continuation",
2,
"bullet '+ '",
),
(
"1. Item text that is long enough to be reflowed when reaching the limit here\n over-indented continuation",
3,
"ordered '1. '",
),
(
"10. Item text that is long enough to be reflowed when reaching the limit here\n over-indented continuation",
4,
"ordered '10. '",
),
];
for (content, expected_indent, description) in &cases {
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
if !result.is_empty() {
let fix = result[0].fix.as_ref().expect("Should have a fix");
for line in fix.replacement.lines().skip(1) {
if !line.is_empty() {
let leading_spaces = line.len() - line.trim_start_matches(' ').len();
assert_eq!(
leading_spaces, *expected_indent,
"For {description}: continuation should have {expected_indent} spaces, got {leading_spaces} in line {:?}\nFull fix: {}",
line, fix.replacement
);
}
}
}
}
}
#[cfg(test)]
mod test_task_list_reflow {
use super::*;
use crate::config::MarkdownFlavor;
use crate::lint_context::LintContext;
fn make_rule(line_length: usize) -> MD013LineLength {
MD013LineLength::from_config_struct(MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(line_length),
..Default::default()
})
}
#[test]
fn test_task_item_long_url_no_warning() {
let rule = make_rule(80);
let content = "- [ ] [some article](https://stackoverflow.blog/2020/11/25/how-to-write-an-effective-developer-resume-advice-from-a-hiring-manager/)\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Task item with long URL should not trigger MD013 (URL exemption): {result:?}"
);
}
#[test]
fn test_task_item_checked_long_url_no_warning() {
let rule = make_rule(80);
for checkbox in ["[x]", "[X]"] {
let content = format!(
"- {checkbox} [some article](https://stackoverflow.blog/2020/11/25/how-to-write-an-effective-developer-resume-advice-from-a-hiring-manager/)\n"
);
let ctx = LintContext::new(&content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Task item with {checkbox} and long URL should not trigger MD013: {result:?}"
);
}
}
#[test]
fn test_task_item_long_text_wraps_correctly() {
let rule = make_rule(80);
let content = "- [ ] This task has a really long description that exceeds the line limit and should be wrapped at the boundary\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Long-text task item should trigger MD013");
let fix = result[0].fix.as_ref().expect("Should have fix");
for line in fix.replacement.lines().skip(1) {
if !line.is_empty() {
assert!(
line.starts_with(" ") && !line.starts_with(" "),
"Continuation should have exactly 6-space indent for '- [ ] ' prefix, got: {line:?}"
);
}
}
}
#[test]
fn test_task_item_fix_does_not_corrupt_checkbox() {
let rule = make_rule(80);
let content = "- [ ] This task has a really long description that exceeds the line limit and should be wrapped at the boundary\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
if let Some(warning) = result.first()
&& let Some(fix) = &warning.fix
{
assert!(
!fix.replacement.contains("[]"),
"Fix must not corrupt '[ ]' to '[]': {}",
fix.replacement
);
assert!(
fix.replacement.starts_with("- [ ] "),
"Fix must preserve task checkbox: {}",
fix.replacement
);
}
}
#[test]
fn test_task_item_all_bullet_markers() {
let rule = make_rule(80);
let url = "https://stackoverflow.blog/2020/11/25/how-to-write-an-effective-developer-resume-advice-from-a-hiring-manager/";
for bullet in ["-", "*", "+"] {
let content = format!("{bullet} [ ] [article]({url})\n");
let ctx = LintContext::new(&content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"'{bullet} [ ]' task item with long URL should not trigger MD013: {result:?}"
);
}
}
}
mod test_github_alert_reflow {
use super::*;
fn make_rule_reflow(line_length: usize) -> MD013LineLength {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(line_length),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
MD013LineLength::from_config_struct(config)
}
#[test]
fn test_github_alert_marker_not_merged_with_content() {
let content = "\
# Heading
> [!NOTE]
> This is alert content that should stay on its own line and not be merged with the NOTE marker above.
";
let rule = make_rule_reflow(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert!(
result.contains("> [!NOTE]\n"),
"[!NOTE] line must remain on its own line; got:\n{result}"
);
assert!(
!result.contains("[!NOTE] This"),
"[!NOTE] must not be merged with content; got:\n{result}"
);
}
#[test]
fn test_all_standard_alert_types_preserved() {
for alert_type in ["NOTE", "TIP", "WARNING", "CAUTION", "IMPORTANT"] {
let content = format!(
"# Heading\n\n> [!{alert_type}]\n> Content for the {alert_type} alert that is quite long and tests wrapping behavior.\n"
);
let rule = make_rule_reflow(80);
let ctx = LintContext::new(&content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert!(
result.contains(&format!("> [!{alert_type}]\n")),
"[!{alert_type}] must remain on its own line; got:\n{result}"
);
}
}
#[test]
fn test_alert_idempotent() {
let content = "\
# Heading
> [!NOTE]
> This is a note with content that is long enough to potentially cause issues if the alert marker gets merged with this line.
Regular paragraph after the alert block.
";
let rule = make_rule_reflow(80);
let ctx1 = LintContext::new(content, MarkdownFlavor::Standard, None);
let first = rule.fix(&ctx1).unwrap();
let ctx2 = LintContext::new(&first, MarkdownFlavor::Standard, None);
let second = rule.fix(&ctx2).unwrap();
assert_eq!(first, second, "Fix must be idempotent for GitHub alert blocks");
}
#[test]
fn test_regular_blockquote_still_reflowed() {
let content = "\
# Heading
> This is a long line in a regular blockquote that
> continues on the next line and together exceeds eighty characters.
";
let rule = make_rule_reflow(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert!(
result.contains("> This is a long line"),
"Regular blockquote content should be preserved; got:\n{result}"
);
assert!(!result.contains("[!"), "No alert markers should appear in result");
}
}
mod reflow_link_exemption_tests {
use super::*;
fn make_rule_reflow_default(line_length: usize) -> MD013LineLength {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(line_length),
reflow: true,
reflow_mode: ReflowMode::Default,
..Default::default()
};
MD013LineLength::from_config_struct(config)
}
fn make_rule_reflow_default_strict(line_length: usize) -> MD013LineLength {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(line_length),
reflow: true,
reflow_mode: ReflowMode::Default,
strict: true,
..Default::default()
};
MD013LineLength::from_config_struct(config)
}
#[test]
fn test_multi_paragraph_list_item_with_link_ref_definition() {
let content = "\
- This is short text.
[very-long-reference-id]: https://example.com/very/long/path/to/some/resource/page
";
let rule = make_rule_reflow_default(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link reference definition in list item should be exempt; got: {result:?}"
);
}
#[test]
fn test_multi_paragraph_list_item_with_standalone_link() {
let content = "\
- This is short text.
[A very long title for a resource article](https://example.com/very/long/path/to/some/resource)
";
let rule = make_rule_reflow_default(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Standalone link in list item should be exempt; got: {result:?}"
);
}
#[test]
fn test_list_item_with_actual_long_text_still_warns() {
let content = "\
- This is a very long paragraph line that definitely exceeds the eighty character limit for this test case right here.
";
let rule = make_rule_reflow_default(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Long text in list item should still trigger a warning"
);
let msg = &result[0].message;
assert!(
msg.contains("exceeds 80 characters"),
"Warning should mention the 80 char limit; got: {msg}"
);
}
#[test]
fn test_multi_paragraph_list_item_long_text_and_link_ref() {
let content = "\
- This is a very long paragraph line that definitely exceeds the eighty character limit for this test case and more.
[ref]: https://example.com/very/long/path/to/some/resource/page/that/is/also/very/long
";
let rule = make_rule_reflow_default(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Long text line should trigger a warning even with exempt link ref"
);
let msg = &result[0].message;
let reported_length: usize = msg.split_whitespace().find_map(|w| w.parse().ok()).unwrap_or(0);
assert!(
reported_length < 150,
"Warning should report actual line length (~113), not combined content; got: {msg}"
);
}
#[test]
fn test_single_paragraph_list_item_with_long_link_ref() {
let content = "\
- [very-long-reference-identifier]: https://example.com/very/long/path/to/some/resource/page
";
let rule = make_rule_reflow_default(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"List item with link ref definition content should be exempt; got: {result:?}"
);
}
#[test]
fn test_link_ref_outside_list_item_exempt() {
let content = "\
[very-long-reference-identifier]: https://example.com/very/long/path/to/some/resource/page
";
let rule = make_rule_reflow_default(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link ref definition outside list should be exempt; got: {result:?}"
);
}
#[test]
fn test_standalone_link_exempt_not_in_strict_mode() {
let content = "\
- This is short text.
[A very long title for a resource article](https://example.com/very/long/path/to/some/resource)
";
let rule = make_rule_reflow_default_strict(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Standalone link in strict mode should NOT be exempt"
);
}
#[test]
fn test_link_ref_exempt_even_in_strict_mode() {
let content = "\
- This is short text.
[very-long-reference-id]: https://example.com/very/long/path/to/some/resource/page
";
let rule = make_rule_reflow_default_strict(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Link ref definition should be exempt even in strict mode; got: {result:?}"
);
}
#[test]
fn test_reflow_default_message_reports_actual_line_length() {
let content = "\
- First paragraph with some reasonably long text that goes over eighty characters for testing purposes.
Second paragraph that is also quite long and exceeds the limit by a fair amount for this test.
";
let rule = make_rule_reflow_default(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should have a warning for long lines");
let msg = &result[0].message;
let reported_length: usize = msg.split_whitespace().find_map(|w| w.parse().ok()).unwrap_or(0);
assert!(
reported_length < 150,
"Message should report individual line length, not combined; got: {msg}"
);
}
fn make_rule_reflow_normalize(line_length: usize) -> MD013LineLength {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(line_length),
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
};
MD013LineLength::from_config_struct(config)
}
#[test]
fn test_normalize_mode_list_item_with_link_ref_def_no_warning() {
let content = "\
- This is short text.
[very-long-reference-id]: https://example.com/very/long/path/to/some/resource/page
";
let rule = make_rule_reflow_normalize(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Normalize mode should not trigger for list item with only one text paragraph and a link ref def; got: {result:?}"
);
}
#[test]
fn test_normalize_mode_list_item_with_standalone_link_no_warning() {
let content = "\
- This is short text.
[A very long title for a resource](https://example.com/very/long/path/to/some/resource)
";
let rule = make_rule_reflow_normalize(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Normalize mode should not trigger for standalone link paragraph; got: {result:?}"
);
}
#[test]
fn test_normalize_mode_list_item_with_actual_multiple_paragraphs_warns() {
let content = "\
- First paragraph text.
Second paragraph text.
";
let rule = make_rule_reflow_normalize(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Normalize mode should trigger for list item with two actual text paragraphs"
);
}
#[test]
fn test_reflow_output_preserves_link_ref_def_when_long_text_triggers() {
let content = "\
- This is a very long paragraph line that definitely exceeds the eighty character limit for this test case and more words.
[ref]: https://example.com/very/long/path/to/some/resource/page/that/is/also/very/long
";
let rule = make_rule_reflow_default(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Long text should trigger warning");
let fix = result[0].fix.as_ref().expect("Should have a fix");
let replacement = &fix.replacement;
assert!(
replacement
.contains("[ref]: https://example.com/very/long/path/to/some/resource/page/that/is/also/very/long"),
"Fix should preserve link ref def verbatim; got:\n{replacement}"
);
}
#[test]
fn test_reflow_output_preserves_standalone_link_when_long_text_triggers() {
let content = "\
- This is a very long paragraph line that definitely exceeds the eighty character limit for this test case and more words.
[A very long title for a resource article](https://example.com/very/long/path/to/some/resource)
";
let rule = make_rule_reflow_default(80);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Long text should trigger warning");
let fix = result[0].fix.as_ref().expect("Should have a fix");
let replacement = &fix.replacement;
assert!(
replacement.contains(
"[A very long title for a resource article](https://example.com/very/long/path/to/some/resource)"
),
"Fix should preserve standalone link verbatim; got:\n{replacement}"
);
}
}
#[test]
fn test_reflow_admonition_in_list_item_basic() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
"\n",
" !!! note\n",
"\n",
" Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains(" !!! note"),
"Admonition header should keep 4-space indent; got:\n{replacement}"
);
assert!(
replacement.contains(" Ut enim ad minim veniam"),
"Admonition body should have 8-space indent; got:\n{replacement}"
);
let body_lines: Vec<&str> = replacement
.lines()
.filter(|l| l.starts_with(" ") && !l.trim().starts_with("!!!"))
.collect();
assert!(
body_lines.len() > 1,
"Admonition body should be wrapped into multiple lines; got:\n{replacement}"
);
}
#[test]
fn test_reflow_collapsible_admonition_in_list_item() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
"\n",
" ??? warning \"Custom Title\"\n",
"\n",
" Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains(" ??? warning \"Custom Title\""),
"Collapsible admonition header should keep indent; got:\n{replacement}"
);
let body_lines: Vec<&str> = replacement
.lines()
.filter(|l| l.starts_with(" ") && !l.trim().starts_with("???"))
.collect();
assert!(
body_lines.len() > 1,
"Collapsible admonition body should be wrapped; got:\n{replacement}"
);
}
#[test]
fn test_reflow_multiple_admonitions_in_list_item() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
"\n",
" !!! note\n",
"\n",
" First admonition body that is long enough to exceed the eighty character line length limit for testing purposes.\n",
"\n",
" !!! warning\n",
"\n",
" Second admonition body that is also long enough to exceed the eighty character line length limit for testing here.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains(" !!! note"),
"First admonition header should be present; got:\n{replacement}"
);
assert!(
replacement.contains(" !!! warning"),
"Second admonition header should be present; got:\n{replacement}"
);
let note_idx = replacement.find(" !!! note").unwrap();
let warning_idx = replacement.find(" !!! warning").unwrap();
let first_body = &replacement[note_idx..warning_idx];
let second_body = &replacement[warning_idx..];
let first_body_lines: Vec<&str> = first_body
.lines()
.filter(|l| l.starts_with(" ") && !l.trim().is_empty())
.collect();
let second_body_lines: Vec<&str> = second_body
.lines()
.filter(|l| l.starts_with(" ") && !l.trim().is_empty())
.collect();
assert!(
first_body_lines.len() > 1,
"First admonition body should be wrapped; got:\n{first_body}"
);
assert!(
second_body_lines.len() > 1,
"Second admonition body should be wrapped; got:\n{second_body}"
);
}
#[test]
fn test_reflow_admonition_short_content_preserved() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
"\n",
" !!! note\n",
"\n",
" Short content.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a fix for the long list item text");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains(" Short content."),
"Short admonition body should be preserved; got:\n{replacement}"
);
}
#[test]
fn test_reflow_admonition_with_multiple_paragraphs() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
"\n",
" !!! note\n",
"\n",
" First paragraph that is long enough to exceed the eighty character line length limit for testing purposes here.\n",
"\n",
" Second paragraph that is also long enough to exceed the eighty character line length limit for proper verification.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a fix");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains(" First paragraph"),
"First paragraph should be present; got:\n{replacement}"
);
assert!(
replacement.contains(" Second paragraph"),
"Second paragraph should be present; got:\n{replacement}"
);
let lines: Vec<&str> = replacement.lines().collect();
let blank_after_first = lines.iter().enumerate().any(|(i, line)| {
line.contains("First paragraph") && {
let mut j = i + 1;
while j < lines.len() && lines[j].starts_with(" ") && !lines[j].trim().is_empty() {
j += 1;
}
j < lines.len() && lines[j].trim().is_empty()
}
});
assert!(
blank_after_first,
"Paragraphs in admonition body should be separated by blank lines; got:\n{replacement}"
);
}
#[test]
fn test_reflow_admonition_not_in_standard_flavor() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
"\n",
" !!! note\n",
"\n",
" Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should still have a fix in standard mode");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
!replacement.is_empty(),
"Should produce non-empty replacement in standard flavor"
);
}
#[test]
fn test_reflow_admonition_idempotent() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
"\n",
" !!! note\n",
"\n",
" Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "First pass should have a fix");
let fix = fixes[0].fix.as_ref().unwrap();
let mut fixed_content = content.to_string();
fixed_content.replace_range(fix.range.clone(), &fix.replacement);
let ctx2 = LintContext::new(&fixed_content, MarkdownFlavor::MkDocs, None);
let result2 = rule.check(&ctx2).unwrap();
let fixes2: Vec<_> = result2.iter().filter(|w| w.fix.is_some()).collect();
assert!(
fixes2.is_empty(),
"Second pass should produce no fixes (idempotent); fixed content:\n{fixed_content}"
);
}
#[test]
fn test_reflow_admonition_only_in_list_no_long_text() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Short list item text.\n",
"\n",
" !!! note\n",
"\n",
" Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a fix for the long admonition body line");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains(" !!! note"),
"Header should be preserved; got:\n{replacement}"
);
let body_lines: Vec<&str> = replacement
.lines()
.filter(|l| l.starts_with(" ") && !l.trim().is_empty())
.collect();
assert!(body_lines.len() > 1, "Body should be wrapped; got:\n{replacement}");
}
#[test]
fn test_reflow_content_after_admonition_in_list_item() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Short item.\n",
"\n",
" !!! note\n",
"\n",
" Body of the admonition that is long enough to need wrapping for testing purposes here in the body.\n",
"\n",
" This paragraph after the admonition should be preserved and not silently dropped.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains(" !!! note"),
"Admonition header should be preserved; got:\n{replacement}"
);
assert!(
replacement.contains(" Body of the admonition"),
"Admonition body should be preserved; got:\n{replacement}"
);
assert!(
replacement.contains("This paragraph after the admonition should be preserved"),
"Trailing paragraph after admonition must not be dropped; got:\n{replacement}"
);
}
#[test]
fn test_reflow_content_after_admonition_short_lines() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Short item.\n",
"\n",
" !!! note\n",
"\n",
" Short body.\n",
"\n",
" Trailing paragraph.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let long_line_warnings: Vec<_> = result.iter().filter(|w| w.message.contains("Line length")).collect();
assert!(
long_line_warnings.is_empty(),
"Short lines should not trigger warnings; got: {long_line_warnings:?}"
);
}
#[test]
fn test_reflow_multiple_blocks_after_admonition() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
"\n",
" !!! note\n",
"\n",
" Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n",
"\n",
" After the admonition, this paragraph text should still be present in the reflowed output and not silently removed.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains(" !!! note"),
"Admonition header should be preserved; got:\n{replacement}"
);
assert!(
replacement.contains("After the admonition"),
"Trailing paragraph must be preserved; got:\n{replacement}"
);
}
#[test]
fn test_reflow_admonition_empty_body() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
"\n",
" !!! note\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix for the long line");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains("!!! note"),
"Empty-body admonition header should be preserved; got:\n{replacement}"
);
}
#[test]
fn test_reflow_admonition_no_blank_line_before_body() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.\n",
"\n",
" !!! note\n",
" Body content immediately following the admonition header without a blank line separator between them.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains("!!! note"),
"Admonition header should be preserved; got:\n{replacement}"
);
assert!(
replacement.contains("Body content immediately"),
"Admonition body should be preserved; got:\n{replacement}"
);
}
#[test]
fn test_reflow_admonition_body_indent_preserved() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Short item.\n",
"\n",
" !!! note\n",
"\n",
" This body line at indent 8 is long enough to exceed the eighty character column limit for testing purposes here.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
for line in replacement.lines() {
if !line.is_empty() && !line.contains("!!!") && !line.starts_with("- ") && !line.starts_with(" ") {
continue;
}
if line.starts_with(" ") && !line.trim().is_empty() && !line.contains("!!!") {
assert!(
line.starts_with(" ") && !line.starts_with(" "),
"Body lines should have exactly 8 spaces of indent; got: '{line}'"
);
}
}
}
#[test]
fn test_reflow_admonition_with_code_block_in_list_item() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(88),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet.\n",
"\n",
" !!! example\n",
"\n",
" ```yaml\n",
" hello: world\n",
" ```\n",
"\n",
" Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains(" ```yaml\n hello: world\n ```"),
"Code block inside admonition must be preserved verbatim; got:\n{replacement}"
);
assert!(
!replacement.contains("``` Lorem"),
"Closing fence must not be merged with paragraph text; got:\n{replacement}"
);
assert!(
replacement.contains(" Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor\n incididunt ut labore et dolore magna aliqua."),
"Trailing paragraph should be reflowed; got:\n{replacement}"
);
}
#[test]
fn test_reflow_admonition_with_tilde_fence_in_list_item() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(88),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet.\n",
"\n",
" !!! example\n",
"\n",
" ~~~python\n",
" def hello():\n",
" pass\n",
" ~~~\n",
"\n",
" Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains(" ~~~python\n def hello():\n pass\n ~~~"),
"Tilde-fenced code block must be preserved; got:\n{replacement}"
);
assert!(
!replacement.contains("~~~ Lorem") && !replacement.contains("~~~Lorem"),
"Closing tilde fence must not be merged with text; got:\n{replacement}"
);
}
#[test]
fn test_reflow_admonition_with_multiple_code_blocks_in_list_item() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(88),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet.\n",
"\n",
" !!! example\n",
"\n",
" ```yaml\n",
" hello: world\n",
" ```\n",
"\n",
" ```python\n",
" print(\"hello\")\n",
" ```\n",
"\n",
" Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains("```yaml"),
"First code block opening fence must be preserved; got:\n{replacement}"
);
assert!(
replacement.contains("```python"),
"Second code block opening fence must be preserved; got:\n{replacement}"
);
assert!(
!replacement.contains("``` Lorem") && !replacement.contains("``` print"),
"Fence markers must not be merged with other content; got:\n{replacement}"
);
}
#[test]
fn test_reflow_admonition_code_block_idempotent() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(88),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet.\n",
"\n",
" !!! example\n",
"\n",
" ```yaml\n",
" hello: world\n",
" ```\n",
"\n",
" Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor\n",
" incididunt ut labore et dolore magna aliqua.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(
fixes.is_empty(),
"Already correctly formatted content should not produce fixes; got {} fix(es)",
fixes.len()
);
}
#[test]
fn test_reflow_tab_container_in_list_item() {
let config = MD013Config {
line_length: crate::types::LineLength::from_const(80),
paragraphs: true,
code_blocks: true,
tables: true,
headings: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::Default,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let rule = MD013LineLength::from_config_struct(config);
let content = concat!(
"# Test\n",
"\n",
"- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
"\n",
" === \"Tab One\"\n",
"\n",
" Tab content here.\n",
);
let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
let fixes: Vec<_> = result.iter().filter(|w| w.fix.is_some()).collect();
assert!(!fixes.is_empty(), "Should have a reflow fix for the long line");
let fix = fixes[0].fix.as_ref().unwrap();
let replacement = &fix.replacement;
assert!(
replacement.contains("=== \"Tab One\"") || replacement.contains("Tab content here"),
"Tab container content should not be silently dropped; got:\n{replacement}"
);
}
#[test]
fn test_md013_links_sorted_by_line_number() {
let content = "\
# Document
See [undefined-ref] for details.
Some text with [another-undef-ref] here.
Short text [link](https://github.com/example/repo/blob/master/keps/sig-node/very-long-name).
";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
for i in 1..ctx.links.len() {
assert!(
ctx.links[i].line >= ctx.links[i - 1].line,
"ctx.links must be sorted by line; link[{}].line={} < link[{}].line={}",
i,
ctx.links[i].line,
i - 1,
ctx.links[i - 1].line,
);
}
for i in 1..ctx.images.len() {
assert!(
ctx.images[i].line >= ctx.images[i - 1].line,
"ctx.images must be sorted by line; image[{}].line={} < image[{}].line={}",
i,
ctx.images[i].line,
i - 1,
ctx.images[i - 1].line,
);
}
}
#[test]
fn test_md013_url_subtraction_with_earlier_reference_links() {
let content = "\
# Document
See [undefined-ref] for details.
Some text with [another-undef-ref] here.
Short text [link](https://github.com/example/repo/blob/master/keps/sig-node/very-long-name).
";
let rule = MD013LineLength::new(80, true, true, true, false);
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
let line7_warnings: Vec<_> = result.iter().filter(|w| w.line == 7).collect();
assert!(
line7_warnings.is_empty(),
"Line 7 should not trigger MD013 — text-only length (excluding URL) is <= 80; got: {:?}",
line7_warnings.iter().map(|w| &w.message).collect::<Vec<_>>()
);
}
#[test]
fn test_reflow_nested_unordered_list_items() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- Short parent.\n\n - This is a very long nested unordered list item that definitely exceeds the eighty character line length limit and should be reflowed.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
let nested_warnings: Vec<_> = result.iter().filter(|w| w.line == 3).collect();
assert!(
!nested_warnings.is_empty(),
"Nested unordered list item exceeding 80 chars should trigger a warning"
);
let fix = nested_warnings[0]
.fix
.as_ref()
.expect("Should have a fix for nested item");
assert!(
fix.replacement.contains('\n'),
"Fix should reflow the long nested item across multiple lines"
);
}
#[test]
fn test_reflow_nested_unordered_matches_ordered() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let unordered = "- parent\n\n - This is a very long nested unordered list item that definitely exceeds the eighty character line length limit and should be reflowed properly.";
let ordered = "1. parent\n\n 1. This is a very long nested ordered list item that definitely exceeds the eighty character line length limit and should be reflowed properly.";
let ctx_u = crate::lint_context::LintContext::new(unordered, crate::config::MarkdownFlavor::Standard, None);
let ctx_o = crate::lint_context::LintContext::new(ordered, crate::config::MarkdownFlavor::Standard, None);
let result_u = rule.check(&ctx_u).unwrap();
let result_o = rule.check(&ctx_o).unwrap();
let nested_u: Vec<_> = result_u.iter().filter(|w| w.line == 3).collect();
let nested_o: Vec<_> = result_o.iter().filter(|w| w.line == 3).collect();
assert!(!nested_u.is_empty(), "Nested unordered item should trigger a warning");
assert!(!nested_o.is_empty(), "Nested ordered item should trigger a warning");
assert!(nested_u[0].fix.is_some(), "Nested unordered should have a fix");
assert!(nested_o[0].fix.is_some(), "Nested ordered should have a fix");
}
#[test]
fn test_reflow_nested_unordered_multiple_items() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- Parent item\n\n - First nested item that is very long and exceeds the eighty character line length limit and needs to be reflowed.\n\n - Second nested item that is also very long and exceeds the eighty character line length limit and needs reflowing too.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
let warn_3: Vec<_> = result.iter().filter(|w| w.line == 3).collect();
let warn_5: Vec<_> = result.iter().filter(|w| w.line == 5).collect();
assert!(!warn_3.is_empty(), "First nested item should trigger a warning");
assert!(!warn_5.is_empty(), "Second nested item should trigger a warning");
}
#[test]
fn test_reflow_nested_unordered_without_blank_line() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- parent\n - This is a very long nested unordered list item that definitely exceeds the eighty character line length limit and should be reflowed.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
let nested_warnings: Vec<_> = result.iter().filter(|w| w.line == 2).collect();
assert!(
!nested_warnings.is_empty(),
"Nested unordered item without blank line should trigger a warning"
);
assert!(
nested_warnings[0].fix.is_some(),
"Should have a fix for the long nested item"
);
}
#[test]
fn test_reflow_deeply_nested_unordered() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- parent\n - child\n - This is a deeply nested grandchild item that is very long and exceeds the eighty character line length limit.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
let nested_warnings: Vec<_> = result.iter().filter(|w| w.line == 3).collect();
assert!(
!nested_warnings.is_empty(),
"Deeply nested grandchild item should trigger a warning"
);
assert!(
nested_warnings[0].fix.is_some(),
"Should have a fix for the long deeply nested item"
);
}
#[test]
fn test_reflow_checkbox_with_continuation_at_base_indent() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- [ ] This is a checkbox item with a very long description that needs to be reflowed properly across lines.\n And this is a continuation line at 2-space indent that should be recognized.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect long checkbox item");
let fix = result[0].fix.as_ref().expect("Should have a fix");
assert!(
fix.replacement.contains("continuation"),
"Fix should include the continuation text: {:?}",
fix.replacement
);
}
#[test]
fn test_reflow_checkbox_with_continuation_at_4_spaces() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- [ ] This is a checkbox item with a very long description that needs to be reflowed properly across lines.\n And this is a continuation line at 4-space indent that should be recognized.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect long checkbox item");
let fix = result[0].fix.as_ref().expect("Should have a fix");
assert!(
fix.replacement.contains("continuation"),
"Fix should include the continuation text: {:?}",
fix.replacement
);
}
#[test]
fn test_reflow_checkbox_output_aligns_with_content() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- [ ] This is a checkbox item with a very long description that definitely exceeds the eighty character line length limit and should be reflowed.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect long checkbox item");
let fix = result[0].fix.as_ref().expect("Should have a fix");
for line in fix.replacement.lines().skip(1) {
if !line.is_empty() {
assert!(
line.starts_with(" ") && !line.starts_with(" "),
"Continuation line should have exactly 6-space indent (marker_len for '- [ ] '), got: {line:?}"
);
}
}
}
#[test]
fn test_reflow_checkbox_mkdocs_continuation() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- [ ] This is a checkbox item with a very long description that needs to be reflowed properly across multiple lines.\n And this continuation at 2-space indent should be collected.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect long checkbox item in MkDocs mode");
let fix = result[0].fix.as_ref().expect("Should have a fix");
assert!(
fix.replacement.contains("continuation"),
"Fix should include the continuation text in MkDocs mode: {:?}",
fix.replacement
);
for line in fix.replacement.lines().skip(1) {
if !line.is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 4,
"MkDocs checkbox continuation should use 4-space indent, got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_reflow_ordered_checkbox_continuation() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. [ ] This is an ordered checkbox item with a very long description that needs to be reflowed properly across lines.\n And this continuation at 3-space indent should be collected.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect long ordered checkbox item");
let fix = result[0].fix.as_ref().expect("Should have a fix");
assert!(
fix.replacement.contains("continuation"),
"Fix should include the continuation text: {:?}",
fix.replacement
);
}
#[test]
fn test_reflow_checkbox_standard_uses_content_aligned_indent() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- [ ] This is a checkbox item with a very long description that needs to be reflowed properly across multiple lines.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect long checkbox item in Standard mode");
let fix = result[0].fix.as_ref().expect("Should have a fix");
for line in fix.replacement.lines().skip(1) {
if !line.is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 6,
"Standard checkbox continuation should use 6-space (content-aligned) indent, got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_reflow_checkbox_mkdocs_semantic_line_breaks() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::SemanticLineBreaks,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- [ ] This is a checkbox item with a very long description that needs to be reflowed properly. And another sentence here.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Should detect long checkbox item in MkDocs semantic mode"
);
let fix = result[0].fix.as_ref().expect("Should have a fix");
for line in fix.replacement.lines().skip(1) {
if !line.is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 4,
"MkDocs checkbox continuation in semantic mode should use 4-space indent, got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_reflow_ordered_checkbox_mkdocs_continuation() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "1. [ ] This is an ordered checkbox item with a very long description that needs to be reflowed properly across multiple lines.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Should detect long ordered checkbox item in MkDocs mode"
);
let fix = result[0].fix.as_ref().expect("Should have a fix");
for line in fix.replacement.lines().skip(1) {
if !line.is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 4,
"MkDocs ordered checkbox continuation should use 4-space indent, got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_reflow_nested_checkbox_mkdocs_continuation() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- Parent item\n - [ ] Nested checkbox item that is very long and needs to wrap across multiple lines properly with correct indentation.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Should detect long nested checkbox item in MkDocs mode"
);
let fix = result[0].fix.as_ref().expect("Should have a fix");
for line in fix.replacement.lines().skip(1) {
if !line.is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 8,
"Nested MkDocs checkbox continuation should use 8-space indent (4 nesting + 4 mkdocs), got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_reflow_checkbox_mkdocs_idempotent() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content =
"- [ ] This checkbox item has a long description that definitely needs to be reflowed across multiple lines.\n";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let first_fix = rule.fix(&ctx).unwrap();
for line in first_fix.lines().skip(1) {
if !line.is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 4,
"First fix should use 4-space indent, got {indent} in: {line:?}"
);
}
}
let ctx2 = crate::lint_context::LintContext::new(&first_fix, crate::config::MarkdownFlavor::MkDocs, None);
let second_fix = rule.fix(&ctx2).unwrap();
assert_eq!(
first_fix, second_fix,
"MkDocs checkbox reflow should be idempotent.\nFirst: {first_fix:?}\nSecond: {second_fix:?}"
);
}
#[test]
fn test_reflow_nested_checkbox_standard_uses_content_aligned() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- Parent item\n - [ ] Nested checkbox item that is very long and needs to wrap across multiple lines properly with content alignment.";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Should detect long nested checkbox in standard mode"
);
let fix = result[0].fix.as_ref().expect("Should have a fix");
for line in fix.replacement.lines().skip(1) {
if !line.is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 10,
"Standard nested checkbox should use 10-space content-aligned indent, got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_reflow_mixed_checkbox_and_regular_mkdocs() {
let config = MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
line_length: crate::types::LineLength::from_const(80),
..Default::default()
};
let rule = MD013LineLength::from_config_struct(config);
let content = "- This regular item is long enough to need wrapping across multiple lines so we can verify indent.\n- [ ] This checkbox item is also long enough to need wrapping across multiple lines to verify indent.\n";
let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let fixed = rule.fix(&ctx).unwrap();
let mut in_checkbox_item = false;
for line in fixed.lines() {
if line.starts_with("- [ ] ") {
in_checkbox_item = true;
} else if line.starts_with("- ") && !line.starts_with("- [") {
in_checkbox_item = false;
} else if !line.is_empty() && line.starts_with(' ') {
let indent = line.len() - line.trim_start().len();
if in_checkbox_item {
assert_eq!(
indent, 4,
"MkDocs checkbox continuation should be 4-space, got {indent} in: {line:?}"
);
}
assert!(
indent >= 2,
"Continuation should be indented at least 2, got {indent} in: {line:?}"
);
}
}
}