From 30d54d41517573081efd4a9d485e6ca08b4ca9a5 Mon Sep 17 00:00:00 2001 From: Chandler Carruth Date: Thu, 9 Jul 2026 01:54:56 +0000 Subject: [PATCH 1/2] fix(MD013): preserve list marker spacing to prevent corruption of nested code blocks When reflowing list items, the linter reconstructed the list marker with a default/configured spacing (e.g. 1 space) without accounting for the original spacing. This caused misalignment and corruption of nested code blocks because the indentation shift was calculated incorrectly. Now we extract and preserve the exact spacing after the bullet or numbered marker, ensuring that continuation lines and nested blocks maintain their relative alignment. Assisted-by: Antigravity with Gemini --- src/rules/md013_line_length/helpers.rs | 17 +++++++++-- src/rules/md013_line_length/tests.rs | 41 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/rules/md013_line_length/helpers.rs b/src/rules/md013_line_length/helpers.rs index 26eea618..131c361b 100644 --- a/src/rules/md013_line_length/helpers.rs +++ b/src/rules/md013_line_length/helpers.rs @@ -132,7 +132,13 @@ pub(crate) fn extract_list_marker_and_content(line: &str) -> (String, String) { ); } } - return (format!("{indent}{bullet}"), trim_preserving_hard_break(rest)); + let spaces_len = rest.len() - rest.trim_start().len(); + let extra_spaces = &rest[..spaces_len]; + let content = &rest[spaces_len..]; + return ( + format!("{indent}{marker_prefix} {extra_spaces}"), + trim_preserving_hard_break(content), + ); } } @@ -158,8 +164,13 @@ pub(crate) fn extract_list_marker_and_content(line: &str) -> (String, String) { ); } } - let content = trim_preserving_hard_break(rest); - return (format!("{indent}{marker_content}"), content); + let spaces_len = rest.len() - rest.trim_start().len(); + let extra_spaces = &rest[..spaces_len]; + let content = &rest[spaces_len..]; + return ( + format!("{indent}{marker_content}{extra_spaces}"), + trim_preserving_hard_break(content), + ); } break; } 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 From 1cf1dc8456c2c9b01e750807090e3a09bc6597fa Mon Sep 17 00:00:00 2001 From: Chandler Carruth Date: Thu, 9 Jul 2026 09:18:28 +0000 Subject: [PATCH 2/2] address PR #710 feedback: refine space extraction and update test expectations 1. Exclude tabs from extra spaces in list markers by using `trim_start_matches(' ')` instead of `trim_start()` in `extract_list_marker_and_content`. 2. Hoist space extraction above GFM checkbox checks so checkboxes with extra preceding spaces (e.g. `- [ ] task`) are correctly recognized and not corrupted during reflow. 3. Update expected outputs in `md013_mkdocs_reflow_test.rs` to preserve list marker spacing, which is the correct behavior for MD013 in isolation (MD030 will eventually normalize the spacing and reindent the body in a full run). Assisted-by: Antigravity with Gemini --- src/rules/md013_line_length/helpers.rs | 58 ++++++++++++++++++----- tests/formats/md013_mkdocs_reflow_test.rs | 12 ++--- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/rules/md013_line_length/helpers.rs b/src/rules/md013_line_length/helpers.rs index 131c361b..3b619374 100644 --- a/src/rules/md013_line_length/helpers.rs +++ b/src/rules/md013_line_length/helpers.rs @@ -123,18 +123,21 @@ 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), ); } } - let spaces_len = rest.len() - rest.trim_start().len(); - let extra_spaces = &rest[..spaces_len]; - let content = &rest[spaces_len..]; return ( format!("{indent}{marker_prefix} {extra_spaces}"), trim_preserving_hard_break(content), @@ -155,18 +158,21 @@ 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 spaces_len = rest.len() - rest.trim_start().len(); - let extra_spaces = &rest[..spaces_len]; - let content = &rest[spaces_len..]; return ( format!("{indent}{marker_content}{extra_spaces}"), trim_preserving_hard_break(content), @@ -549,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/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