diff --git a/src/rules/md013_line_length/helpers.rs b/src/rules/md013_line_length/helpers.rs index 26eea618..3b619374 100644 --- a/src/rules/md013_line_length/helpers.rs +++ b/src/rules/md013_line_length/helpers.rs @@ -123,16 +123,25 @@ pub(crate) fn extract_list_marker_and_content(line: &str) -> (String, String) { for bullet in ["- ", "* ", "+ "] { if let Some(rest) = trimmed.strip_prefix(bullet) { let marker_prefix = &bullet[..bullet.len() - 1]; // "-", "*", or "+" + + // Hoist space extraction and exclude tabs + let spaces_len = rest.len() - rest.trim_start_matches(' ').len(); + let extra_spaces = &rest[..spaces_len]; + let content = &rest[spaces_len..]; + // Include GFM task list checkboxes in the non-wrappable marker prefix for checkbox in ["[ ] ", "[x] ", "[X] "] { - if let Some(content) = rest.strip_prefix(checkbox) { + if let Some(actual_content) = content.strip_prefix(checkbox) { return ( - format!("{indent}{marker_prefix} {checkbox}"), - trim_preserving_hard_break(content), + format!("{indent}{marker_prefix} {extra_spaces}{checkbox}"), + trim_preserving_hard_break(actual_content), ); } } - return (format!("{indent}{bullet}"), trim_preserving_hard_break(rest)); + return ( + format!("{indent}{marker_prefix} {extra_spaces}"), + trim_preserving_hard_break(content), + ); } } @@ -149,17 +158,25 @@ pub(crate) fn extract_list_marker_and_content(line: &str) -> (String, String) { { marker_content.push(next); let rest = chars.as_str(); + + // Hoist space extraction and exclude tabs + let spaces_len = rest.len() - rest.trim_start_matches(' ').len(); + let extra_spaces = &rest[..spaces_len]; + let content = &rest[spaces_len..]; + // Check for GFM task list checkboxes for checkbox in ["[ ] ", "[x] ", "[X] "] { - if let Some(content) = rest.strip_prefix(checkbox) { + if let Some(actual_content) = content.strip_prefix(checkbox) { return ( - format!("{indent}{marker_content}{checkbox}"), - trim_preserving_hard_break(content), + format!("{indent}{marker_content}{extra_spaces}{checkbox}"), + trim_preserving_hard_break(actual_content), ); } } - let content = trim_preserving_hard_break(rest); - return (format!("{indent}{marker_content}"), content); + return ( + format!("{indent}{marker_content}{extra_spaces}"), + trim_preserving_hard_break(content), + ); } break; } @@ -538,6 +555,34 @@ mod tests { extract_list_marker_and_content("99. [x] multi-digit ordered"), ("99. [x] ".to_string(), "multi-digit ordered".to_string()) ); + + // Extra spaces preservation + assert_eq!( + extract_list_marker_and_content("- item"), + ("- ".to_string(), "item".to_string()) + ); + assert_eq!( + extract_list_marker_and_content("1. item"), + ("1. ".to_string(), "item".to_string()) + ); + assert_eq!( + extract_list_marker_and_content("- [ ] item"), + ("- [ ] ".to_string(), "item".to_string()) + ); + assert_eq!( + extract_list_marker_and_content("1. [ ] item"), + ("1. [ ] ".to_string(), "item".to_string()) + ); + + // Tabs should not be preserved in marker + assert_eq!( + extract_list_marker_and_content("- \titem"), + ("- ".to_string(), "\titem".to_string()) + ); + assert_eq!( + extract_list_marker_and_content("1. \titem"), + ("1. ".to_string(), "\titem".to_string()) + ); } #[test] diff --git a/src/rules/md013_line_length/tests.rs b/src/rules/md013_line_length/tests.rs index 2c7fb340..af385cd5 100644 --- a/src/rules/md013_line_length/tests.rs +++ b/src/rules/md013_line_length/tests.rs @@ -1043,6 +1043,47 @@ And a bullet list: } } +#[test] +fn test_text_reflow_preserves_nested_code_blocks_in_lists() { + let config = MD013Config { + line_length: crate::types::LineLength::from_const(40), + reflow: true, + ..Default::default() + }; + let mut rule = MD013LineLength::from_config_struct(config); + rule.list_spacing = MD030Config { + ul_single: crate::types::PositiveUsize::from_const(3), + ul_multi: crate::types::PositiveUsize::from_const(3), + ..Default::default() + }; + + let content = indoc! {" + - This is a very long list item text that will definitely exceed the forty character limit and force a reflow. + + ``` + code block line 1 + code block line 2 + ``` + "}; + + let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); + let fixed = rule.fix(&ctx).unwrap(); + + let expected = indoc! {" + - This is a very long list item text + that will definitely exceed the + forty character limit and force a + reflow. + + ``` + code block line 1 + code block line 2 + ``` + "}; + + assert_eq!(fixed, expected); +} + #[test] fn test_issue_83_numbered_list_with_backticks() { // Test for issue #83: enable_reflow was incorrectly handling numbered lists diff --git a/tests/formats/md013_mkdocs_reflow_test.rs b/tests/formats/md013_mkdocs_reflow_test.rs index 3e82d671..7dbc089a 100644 --- a/tests/formats/md013_mkdocs_reflow_test.rs +++ b/tests/formats/md013_mkdocs_reflow_test.rs @@ -1332,7 +1332,7 @@ fn test_issue_509_exact_repro_4space_list_admonition_code_block() { let expected = "\ # Test -1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. !!! note @@ -1372,7 +1372,7 @@ fn test_issue_509_unordered_list_variant() { let fixed = rule.fix(&ctx).unwrap(); let expected = "\ -- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. !!! note @@ -1413,7 +1413,7 @@ fn test_issue_509_admonition_code_block_long_text_after_4space() { let fixed = rule.fix(&ctx).unwrap(); let expected = "\ -1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. !!! note @@ -1496,7 +1496,7 @@ fn test_issue_509_code_block_with_blank_lines_and_deep_indent() { // MD030 normalizes "1. " to "1. " — only the overlong line and // marker spacing should change, everything else verbatim. let expected = "\ -1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. !!! note @@ -1546,7 +1546,7 @@ fn test_issue_509_mixed_fence_types_not_confused() { let fixed = rule.fix(&ctx).unwrap(); let expected = "\ -1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. !!! note @@ -1595,7 +1595,7 @@ fn test_issue_509_info_string_not_treated_as_closing_fence() { let fixed = rule.fix(&ctx).unwrap(); let expected = "\ -1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. !!! note