rumdl 0.0.80__tar.gz → 0.0.81__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rumdl might be problematic. Click here for more details.

Files changed (279) hide show
  1. {rumdl-0.0.80 → rumdl-0.0.81}/Cargo.lock +1 -1
  2. {rumdl-0.0.80 → rumdl-0.0.81}/Cargo.toml +1 -1
  3. {rumdl-0.0.80 → rumdl-0.0.81}/PKG-INFO +1 -1
  4. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md022_blanks_around_headings.rs +163 -249
  5. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md040_fenced_code_language.rs +70 -8
  6. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md055_table_pipe_style.rs +23 -13
  7. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/fix_utils.rs +48 -2
  8. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/mod.rs +12 -0
  9. {rumdl-0.0.80 → rumdl-0.0.81}/tests/cli_lsp_fix_consistency.rs +54 -54
  10. rumdl-0.0.81/tests/configuration_inheritance_tests.rs +326 -0
  11. rumdl-0.0.81/tests/cross_platform_compatibility_tests.rs +417 -0
  12. rumdl-0.0.81/tests/final_confidence_assessment.rs +512 -0
  13. rumdl-0.0.81/tests/lsp_editor_integration_tests.rs +326 -0
  14. rumdl-0.0.81/tests/lsp_memory_leak_tests.rs +396 -0
  15. rumdl-0.0.81/tests/malformed_markdown_stress_tests.rs +314 -0
  16. rumdl-0.0.81/tests/performance_validation_tests.rs +232 -0
  17. rumdl-0.0.81/tests/real_world_repository_tests.rs +447 -0
  18. rumdl-0.0.81/tests/thread_safety_tests.rs +416 -0
  19. rumdl-0.0.81/tests/unicode_edge_case_tests.rs +166 -0
  20. {rumdl-0.0.80 → rumdl-0.0.81}/.rumdl.toml +0 -0
  21. {rumdl-0.0.80 → rumdl-0.0.81}/MANIFEST.in +0 -0
  22. {rumdl-0.0.80 → rumdl-0.0.81}/Makefile +0 -0
  23. {rumdl-0.0.80 → rumdl-0.0.81}/README.md +0 -0
  24. {rumdl-0.0.80 → rumdl-0.0.81}/assets/logo.png +0 -0
  25. {rumdl-0.0.80 → rumdl-0.0.81}/benches/fix_performance.rs +0 -0
  26. {rumdl-0.0.80 → rumdl-0.0.81}/benches/range_performance.rs +0 -0
  27. {rumdl-0.0.80 → rumdl-0.0.81}/benches/range_utils_benchmark.rs +0 -0
  28. {rumdl-0.0.80 → rumdl-0.0.81}/benches/rule_performance.rs +0 -0
  29. {rumdl-0.0.80 → rumdl-0.0.81}/benches/simple_fix_bench.rs +0 -0
  30. {rumdl-0.0.80 → rumdl-0.0.81}/docs/RULES.md +0 -0
  31. {rumdl-0.0.80 → rumdl-0.0.81}/docs/global-settings.md +0 -0
  32. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md001.md +0 -0
  33. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md002.md +0 -0
  34. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md003.md +0 -0
  35. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md004.md +0 -0
  36. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md005.md +0 -0
  37. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md006.md +0 -0
  38. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md007.md +0 -0
  39. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md009.md +0 -0
  40. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md010.md +0 -0
  41. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md011.md +0 -0
  42. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md012.md +0 -0
  43. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md013.md +0 -0
  44. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md014.md +0 -0
  45. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md018.md +0 -0
  46. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md019.md +0 -0
  47. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md020.md +0 -0
  48. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md021.md +0 -0
  49. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md022.md +0 -0
  50. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md023.md +0 -0
  51. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md024.md +0 -0
  52. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md025.md +0 -0
  53. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md026.md +0 -0
  54. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md027.md +0 -0
  55. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md028.md +0 -0
  56. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md029.md +0 -0
  57. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md030.md +0 -0
  58. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md031.md +0 -0
  59. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md032.md +0 -0
  60. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md033.md +0 -0
  61. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md034.md +0 -0
  62. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md035.md +0 -0
  63. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md036.md +0 -0
  64. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md037.md +0 -0
  65. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md038.md +0 -0
  66. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md039.md +0 -0
  67. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md040.md +0 -0
  68. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md041.md +0 -0
  69. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md042.md +0 -0
  70. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md043.md +0 -0
  71. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md044.md +0 -0
  72. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md045.md +0 -0
  73. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md046.md +0 -0
  74. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md047.md +0 -0
  75. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md048.md +0 -0
  76. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md049.md +0 -0
  77. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md050.md +0 -0
  78. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md051.md +0 -0
  79. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md052.md +0 -0
  80. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md053.md +0 -0
  81. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md054.md +0 -0
  82. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md055.md +0 -0
  83. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md056.md +0 -0
  84. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md057.md +0 -0
  85. {rumdl-0.0.80 → rumdl-0.0.81}/docs/md058.md +0 -0
  86. {rumdl-0.0.80 → rumdl-0.0.81}/issues/plan-rule-parity-with-markdownlint.md +0 -0
  87. {rumdl-0.0.80 → rumdl-0.0.81}/parity_check.py +0 -0
  88. {rumdl-0.0.80 → rumdl-0.0.81}/pyproject.toml +0 -0
  89. {rumdl-0.0.80 → rumdl-0.0.81}/python/MANIFEST.in +0 -0
  90. {rumdl-0.0.80 → rumdl-0.0.81}/python/PYTHON-README.md +0 -0
  91. {rumdl-0.0.80 → rumdl-0.0.81}/python/rumdl/__init__.py +0 -0
  92. {rumdl-0.0.80 → rumdl-0.0.81}/python/rumdl/__main__.py +0 -0
  93. {rumdl-0.0.80 → rumdl-0.0.81}/python/rumdl/py.typed +0 -0
  94. {rumdl-0.0.80 → rumdl-0.0.81}/rumdl.toml.example +0 -0
  95. {rumdl-0.0.80 → rumdl-0.0.81}/src/config.rs +0 -0
  96. {rumdl-0.0.80 → rumdl-0.0.81}/src/init.rs +0 -0
  97. {rumdl-0.0.80 → rumdl-0.0.81}/src/lib.rs +0 -0
  98. {rumdl-0.0.80 → rumdl-0.0.81}/src/lint_context.rs +0 -0
  99. {rumdl-0.0.80 → rumdl-0.0.81}/src/lsp/mod.rs +0 -0
  100. {rumdl-0.0.80 → rumdl-0.0.81}/src/lsp/server.rs +0 -0
  101. {rumdl-0.0.80 → rumdl-0.0.81}/src/lsp/types.rs +0 -0
  102. {rumdl-0.0.80 → rumdl-0.0.81}/src/main.rs +0 -0
  103. {rumdl-0.0.80 → rumdl-0.0.81}/src/markdownlint_config.rs +0 -0
  104. {rumdl-0.0.80 → rumdl-0.0.81}/src/parallel.rs +0 -0
  105. {rumdl-0.0.80 → rumdl-0.0.81}/src/performance.rs +0 -0
  106. {rumdl-0.0.80 → rumdl-0.0.81}/src/profiling.rs +0 -0
  107. {rumdl-0.0.80 → rumdl-0.0.81}/src/python.rs +0 -0
  108. {rumdl-0.0.80 → rumdl-0.0.81}/src/rule.rs +0 -0
  109. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/blockquote_utils.rs +0 -0
  110. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/code_block_utils.rs +0 -0
  111. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/code_fence_utils.rs +0 -0
  112. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/emphasis_style.rs +0 -0
  113. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/front_matter_utils.rs +0 -0
  114. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/heading_utils.rs +0 -0
  115. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/list_utils.rs +0 -0
  116. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md001_heading_increment.rs +0 -0
  117. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md002_first_heading_h1.rs +0 -0
  118. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md003_heading_style.rs +0 -0
  119. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md004_unordered_list_style.rs +0 -0
  120. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md005_list_indent.rs +0 -0
  121. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md006_start_bullets.rs +0 -0
  122. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md007_ul_indent.rs +0 -0
  123. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md009_trailing_spaces.rs +0 -0
  124. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md010_no_hard_tabs.rs +0 -0
  125. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md011_no_reversed_links.rs +0 -0
  126. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md012_no_multiple_blanks.rs +0 -0
  127. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md013_line_length.rs +0 -0
  128. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md014_commands_show_output.rs +0 -0
  129. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md018_no_missing_space_atx.rs +0 -0
  130. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md019_no_multiple_space_atx.rs +0 -0
  131. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md020_no_missing_space_closed_atx.rs +0 -0
  132. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md021_no_multiple_space_closed_atx.rs +0 -0
  133. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md023_heading_start_left.rs +0 -0
  134. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md024_no_duplicate_heading.rs +0 -0
  135. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md025_single_title.rs +0 -0
  136. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md026_no_trailing_punctuation.rs +0 -0
  137. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md027_multiple_spaces_blockquote.rs +0 -0
  138. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md028_no_blanks_blockquote.rs +0 -0
  139. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md029_ordered_list_prefix.rs +0 -0
  140. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md030_list_marker_space.rs +0 -0
  141. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md031_blanks_around_fences.rs +0 -0
  142. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md032_blanks_around_lists.rs +0 -0
  143. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md033_no_inline_html.rs +0 -0
  144. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md034_no_bare_urls.rs +0 -0
  145. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md035_hr_style.rs +0 -0
  146. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md036_no_emphasis_only_first.rs +0 -0
  147. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md037_spaces_around_emphasis.rs +0 -0
  148. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md038_no_space_in_code.rs +0 -0
  149. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md039_no_space_in_links.rs +0 -0
  150. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md041_first_line_heading.rs +0 -0
  151. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md042_no_empty_links.rs +0 -0
  152. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md043_required_headings.rs +0 -0
  153. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md044_proper_names.rs +0 -0
  154. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md045_no_alt_text.rs +0 -0
  155. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md046_code_block_style.rs +0 -0
  156. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md047_single_trailing_newline.rs +0 -0
  157. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md048_code_fence_style.rs +0 -0
  158. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md049_emphasis_style.rs +0 -0
  159. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md050_strong_style.rs +0 -0
  160. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md051_link_fragments.rs +0 -0
  161. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md052_reference_links_images.rs +0 -0
  162. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md053_link_image_reference_definitions.rs +0 -0
  163. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md054_link_image_style.rs +0 -0
  164. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md056_table_column_count.rs +0 -0
  165. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md057_existing_relative_links.rs +0 -0
  166. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/md058_blanks_around_tables.rs +0 -0
  167. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/mod.rs +0 -0
  168. {rumdl-0.0.80 → rumdl-0.0.81}/src/rules/strong_style.rs +0 -0
  169. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/ast_utils.rs +0 -0
  170. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/code_block_utils.rs +0 -0
  171. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/document_structure.rs +0 -0
  172. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/early_returns.rs +0 -0
  173. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/element_cache.rs +0 -0
  174. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/markdown_elements.rs +0 -0
  175. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/range_utils.rs +0 -0
  176. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/regex_cache.rs +0 -0
  177. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/string_interner.rs +0 -0
  178. {rumdl-0.0.80 → rumdl-0.0.81}/src/utils/table_utils.rs +0 -0
  179. {rumdl-0.0.80 → rumdl-0.0.81}/tests/advanced_integration_tests.rs +0 -0
  180. {rumdl-0.0.80 → rumdl-0.0.81}/tests/character_ranges/additional_tests.rs +0 -0
  181. {rumdl-0.0.80 → rumdl-0.0.81}/tests/character_ranges/basic_tests.rs +0 -0
  182. {rumdl-0.0.80 → rumdl-0.0.81}/tests/character_ranges/comprehensive_tests.rs +0 -0
  183. {rumdl-0.0.80 → rumdl-0.0.81}/tests/character_ranges/extended_tests.rs +0 -0
  184. {rumdl-0.0.80 → rumdl-0.0.81}/tests/character_ranges/mod.rs +0 -0
  185. {rumdl-0.0.80 → rumdl-0.0.81}/tests/character_ranges/unicode_utils.rs +0 -0
  186. {rumdl-0.0.80 → rumdl-0.0.81}/tests/cli_duplication_test.rs +0 -0
  187. {rumdl-0.0.80 → rumdl-0.0.81}/tests/cli_integration_tests.rs +0 -0
  188. {rumdl-0.0.80 → rumdl-0.0.81}/tests/commonmark_compliance_tests.rs +0 -0
  189. {rumdl-0.0.80 → rumdl-0.0.81}/tests/comprehensive_integration_tests.rs +0 -0
  190. {rumdl-0.0.80 → rumdl-0.0.81}/tests/config_application_tests.rs +0 -0
  191. {rumdl-0.0.80 → rumdl-0.0.81}/tests/config_file_command_test.rs +0 -0
  192. {rumdl-0.0.80 → rumdl-0.0.81}/tests/config_tests.rs +0 -0
  193. {rumdl-0.0.80 → rumdl-0.0.81}/tests/consistency_regression_tests.rs +0 -0
  194. {rumdl-0.0.80 → rumdl-0.0.81}/tests/init_command_test.rs +0 -0
  195. {rumdl-0.0.80 → rumdl-0.0.81}/tests/init_tests.rs +0 -0
  196. {rumdl-0.0.80 → rumdl-0.0.81}/tests/integration_tests.rs +0 -0
  197. {rumdl-0.0.80 → rumdl-0.0.81}/tests/json_output_test.rs +0 -0
  198. {rumdl-0.0.80 → rumdl-0.0.81}/tests/lib.rs +0 -0
  199. {rumdl-0.0.80 → rumdl-0.0.81}/tests/lsp_integration_tests.rs +0 -0
  200. {rumdl-0.0.80 → rumdl-0.0.81}/tests/lsp_tests.rs +0 -0
  201. {rumdl-0.0.80 → rumdl-0.0.81}/tests/markdownlint_cli_integration.rs +0 -0
  202. {rumdl-0.0.80 → rumdl-0.0.81}/tests/markdownlint_config_test.rs +0 -0
  203. {rumdl-0.0.80 → rumdl-0.0.81}/tests/md030_edge_cases.md +0 -0
  204. {rumdl-0.0.80 → rumdl-0.0.81}/tests/output_format_tests.rs +0 -0
  205. {rumdl-0.0.80 → rumdl-0.0.81}/tests/perf_check.rs +0 -0
  206. {rumdl-0.0.80 → rumdl-0.0.81}/tests/pyproject_config_tests.rs +0 -0
  207. {rumdl-0.0.80 → rumdl-0.0.81}/tests/regression_prevention_tests.rs +0 -0
  208. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md001_test.rs +0 -0
  209. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md001_unicode_test.rs +0 -0
  210. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md002_test.rs +0 -0
  211. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md003_test.rs +0 -0
  212. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md004_test.rs +0 -0
  213. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md005_test.rs +0 -0
  214. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md006_test.rs +0 -0
  215. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md006_unicode_test.rs +0 -0
  216. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md007_test.rs +0 -0
  217. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md009_test.rs +0 -0
  218. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md010_test.rs +0 -0
  219. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md011_test.rs +0 -0
  220. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md012_test.rs +0 -0
  221. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md013_test.rs +0 -0
  222. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md014_test.rs +0 -0
  223. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md018_test.rs +0 -0
  224. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md019_test.rs +0 -0
  225. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md020_test.rs +0 -0
  226. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md021_test.rs +0 -0
  227. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md022_test.rs +0 -0
  228. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md023_extended_test.rs +0 -0
  229. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md023_test.rs +0 -0
  230. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md024_test.rs +0 -0
  231. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md025_test.rs +0 -0
  232. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md026_test.rs +0 -0
  233. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md027_test.rs +0 -0
  234. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md028_test.rs +0 -0
  235. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md029_test.rs +0 -0
  236. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md030_test.rs +0 -0
  237. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md031_test.rs +0 -0
  238. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md032_test.rs +0 -0
  239. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md033_extended_test.rs +0 -0
  240. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md033_test.rs +0 -0
  241. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md034_test.rs +0 -0
  242. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md035_test.rs +0 -0
  243. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md036_test.rs +0 -0
  244. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md037_test.rs +0 -0
  245. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md038_test.rs +0 -0
  246. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md039_test.rs +0 -0
  247. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md040_test.rs +0 -0
  248. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md041_test.rs +0 -0
  249. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md042_test.rs +0 -0
  250. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md043_test.rs +0 -0
  251. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md044_test.rs +0 -0
  252. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md045_test.rs +0 -0
  253. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md046_test.rs +0 -0
  254. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md047_test.rs +0 -0
  255. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md048_test.rs +0 -0
  256. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md049_test.rs +0 -0
  257. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md050_test.rs +0 -0
  258. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md051_test.rs +0 -0
  259. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md052_test.rs +0 -0
  260. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md053_additional_test.rs +0 -0
  261. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md053_proptest.rs +0 -0
  262. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md053_test.rs +0 -0
  263. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md054_test.rs +0 -0
  264. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md054_unicode_test.rs +0 -0
  265. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md055_test.rs +0 -0
  266. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md056_test.rs +0 -0
  267. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md057_test.rs +0 -0
  268. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/md058_test.rs +0 -0
  269. {rumdl-0.0.80 → rumdl-0.0.81}/tests/rules/mod.rs +0 -0
  270. {rumdl-0.0.80 → rumdl-0.0.81}/tests/utils/blockquote_utils_test.rs +0 -0
  271. {rumdl-0.0.80 → rumdl-0.0.81}/tests/utils/code_block_utils_extended_test.rs +0 -0
  272. {rumdl-0.0.80 → rumdl-0.0.81}/tests/utils/code_block_utils_test.rs +0 -0
  273. {rumdl-0.0.80 → rumdl-0.0.81}/tests/utils/core_utils_test.rs +0 -0
  274. {rumdl-0.0.80 → rumdl-0.0.81}/tests/utils/front_matter_utils_test.rs +0 -0
  275. {rumdl-0.0.80 → rumdl-0.0.81}/tests/utils/line_index_test.rs +0 -0
  276. {rumdl-0.0.80 → rumdl-0.0.81}/tests/utils/mod.rs +0 -0
  277. {rumdl-0.0.80 → rumdl-0.0.81}/tests/utils_markdown_edge_cases.rs +0 -0
  278. {rumdl-0.0.80 → rumdl-0.0.81}/tests/utils_tests.rs +0 -0
  279. {rumdl-0.0.80 → rumdl-0.0.81}/tests/vscode_extension_fixes.rs +0 -0
@@ -1783,7 +1783,7 @@ dependencies = [
1783
1783
 
1784
1784
  [[package]]
1785
1785
  name = "rumdl"
1786
- version = "0.0.80"
1786
+ version = "0.0.81"
1787
1787
  dependencies = [
1788
1788
  "anyhow",
1789
1789
  "assert_cmd",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rumdl"
3
- version = "0.0.80"
3
+ version = "0.0.81"
4
4
  edition = "2021"
5
5
  description = "A fast Markdown linter written in Rust (Ru(st) MarkDown Linter)"
6
6
  authors = ["Ruben J. Jongejan <ruben.jongejan@gmail.com>"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rumdl
3
- Version: 0.0.80
3
+ Version: 0.0.81
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -4,7 +4,7 @@
4
4
  use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
5
5
  use crate::rules::heading_utils::{is_heading, is_setext_heading_marker};
6
6
  use crate::utils::document_structure::{DocumentStructure, DocumentStructureExtensions};
7
- use crate::utils::range_utils::{calculate_heading_range, LineIndex};
7
+ use crate::utils::range_utils::{calculate_heading_range};
8
8
  use fancy_regex::Regex;
9
9
  use lazy_static::lazy_static;
10
10
  use toml;
@@ -251,7 +251,9 @@ impl MD022BlanksAroundHeadings {
251
251
  }
252
252
 
253
253
  /// Fix a document by adding appropriate blank lines around headings
254
- fn _fix_content(&self, lines: &[&str]) -> String {
254
+ fn _fix_content(&self, lines: &[&str], original_content: &str) -> String {
255
+ let line_ending = crate::utils::detect_line_ending(original_content);
256
+ let had_trailing_newline = original_content.ends_with('\n') || original_content.ends_with("\r\n");
255
257
  let mut result = Vec::new();
256
258
  let mut in_code_block = false;
257
259
  let mut fence_char = None;
@@ -463,7 +465,21 @@ impl MD022BlanksAroundHeadings {
463
465
  }
464
466
  }
465
467
 
466
- result.join("\n")
468
+ let joined = result.join(line_ending);
469
+
470
+ // Preserve original trailing newline behavior
471
+ if had_trailing_newline && !joined.ends_with('\n') && !joined.ends_with("\r\n") {
472
+ format!("{}{}", joined, line_ending)
473
+ } else if !had_trailing_newline && (joined.ends_with('\n') || joined.ends_with("\r\n")) {
474
+ // Remove trailing newline if original didn't have one
475
+ if joined.ends_with("\r\n") {
476
+ joined[..joined.len()-2].to_string()
477
+ } else {
478
+ joined[..joined.len()-1].to_string()
479
+ }
480
+ } else {
481
+ joined
482
+ }
467
483
  }
468
484
  }
469
485
 
@@ -478,6 +494,7 @@ impl Rule for MD022BlanksAroundHeadings {
478
494
 
479
495
  fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
480
496
  let content = ctx.content;
497
+ let line_ending = crate::utils::detect_line_ending(content);
481
498
  let mut result = Vec::new();
482
499
  let lines: Vec<&str> = content.lines().collect();
483
500
 
@@ -486,33 +503,30 @@ impl Rule for MD022BlanksAroundHeadings {
486
503
  return Ok(result);
487
504
  }
488
505
 
506
+ // Early return for files without headings
507
+ if !content.contains('#') && !content.contains('=') && !content.contains('-') {
508
+ return Ok(result);
509
+ }
510
+
511
+ // Pre-compute line byte offsets for efficient range calculations
512
+ let mut line_starts = Vec::with_capacity(lines.len() + 1);
513
+ let mut current_pos = 0;
514
+ line_starts.push(0);
515
+
516
+ for line in &lines {
517
+ current_pos += line.len() + line_ending.len();
518
+ line_starts.push(current_pos);
519
+ }
520
+
521
+ // Pre-compute which lines are blank for efficient lookup
522
+ let blank_lines: Vec<bool> = lines.iter().map(|line| line.trim().is_empty()).collect();
523
+
524
+ // Pre-compute code block regions for efficient lookup
525
+ let mut code_block_lines = vec![false; lines.len()];
489
526
  let mut in_code_block = false;
490
527
  let mut fence_char = None;
491
- let mut in_front_matter = false;
492
- let mut front_matter_start_detected = false;
493
- let mut _is_first_line = true;
494
- let mut prev_heading_index: Option<usize> = None;
495
- let mut processed_headings = std::collections::HashSet::new();
496
528
 
497
529
  for (i, line) in lines.iter().enumerate() {
498
- // Handle front matter - only consider it front matter if at the start
499
- if FRONT_MATTER_PATTERN.is_match(line).unwrap_or(false) {
500
- // Only start front matter if at the beginning of the document (allowing for blank lines)
501
- if !front_matter_start_detected && i == 0
502
- || (i > 0 && lines[..i].iter().all(|l| l.trim().is_empty()))
503
- {
504
- in_front_matter = true;
505
- front_matter_start_detected = true;
506
- } else if in_front_matter {
507
- // End front matter if we're in it
508
- in_front_matter = false;
509
- }
510
- // Otherwise it's just a horizontal rule, not front matter
511
- _is_first_line = false;
512
- continue;
513
- }
514
-
515
- // Check for code block fences
516
530
  let trimmed = line.trim();
517
531
  let is_code_fence = (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
518
532
  && (trimmed == "```"
@@ -524,7 +538,6 @@ impl Rule for MD022BlanksAroundHeadings {
524
538
  .map_or(true, |c| c.is_whitespace() || c.is_alphabetic()));
525
539
 
526
540
  if is_code_fence {
527
- // Toggle code block state and update fence character
528
541
  if !in_code_block {
529
542
  fence_char = Some(&trimmed[..3]);
530
543
  in_code_block = true;
@@ -534,32 +547,46 @@ impl Rule for MD022BlanksAroundHeadings {
534
547
  fence_char = None;
535
548
  }
536
549
  }
537
- continue;
538
550
  }
539
551
 
540
- if in_code_block || in_front_matter {
541
- continue;
552
+ code_block_lines[i] = in_code_block;
553
+ }
554
+
555
+ // Pre-compute front matter region
556
+ let mut front_matter_lines = vec![false; lines.len()];
557
+ let mut in_front_matter = false;
558
+ let mut front_matter_start_detected = false;
559
+
560
+ for (i, line) in lines.iter().enumerate() {
561
+ if FRONT_MATTER_PATTERN.is_match(line).unwrap_or(false) {
562
+ if !front_matter_start_detected && (i == 0 || lines[..i].iter().all(|l| l.trim().is_empty())) {
563
+ in_front_matter = true;
564
+ front_matter_start_detected = true;
565
+ } else if in_front_matter {
566
+ in_front_matter = false;
567
+ }
542
568
  }
569
+ front_matter_lines[i] = in_front_matter;
570
+ }
571
+
572
+ // Collect all headings first to batch process
573
+ let mut heading_violations = Vec::new();
574
+ let mut processed_headings = std::collections::HashSet::new();
543
575
 
544
- _is_first_line = false;
576
+ for (i, line) in lines.iter().enumerate() {
577
+ // Skip code blocks and front matter
578
+ if code_block_lines[i] || front_matter_lines[i] {
579
+ continue;
580
+ }
545
581
 
546
582
  // Check if it's a heading
547
- if is_heading(line)
548
- || (i > 0
549
- && is_setext_heading_marker(line)
550
- && is_heading(&format!("{} {}", lines[i - 1], line)))
551
- {
552
- let heading_line = if is_setext_heading_marker(line) {
553
- i - 1
554
- } else {
555
- i
556
- };
557
- let heading_display_line = heading_line + 1; // 1-indexed for display
583
+ let is_atx_heading = is_heading(line);
584
+ let is_setext_heading = i > 0
585
+ && is_setext_heading_marker(line)
586
+ && is_heading(&format!("{} {}", lines[i - 1], line));
558
587
 
559
- // Skip non-heading lines
560
- if line.trim().is_empty() {
561
- continue;
562
- }
588
+ if is_atx_heading || is_setext_heading {
589
+ let heading_line = if is_setext_heading { i - 1 } else { i };
563
590
 
564
591
  // Skip if we've already processed this heading
565
592
  if processed_headings.contains(&heading_line) {
@@ -568,230 +595,127 @@ impl Rule for MD022BlanksAroundHeadings {
568
595
 
569
596
  processed_headings.insert(heading_line);
570
597
 
571
- // Track issues for this heading
572
- let mut issues = Vec::new();
573
-
574
- // Check consecutive headings
575
- if let Some(prev_idx) = prev_heading_index {
576
- let blanks_between = heading_line - prev_idx - 1;
577
- let required_blanks = self.lines_above.max(self.lines_below);
578
-
579
- if blanks_between < required_blanks {
580
- let line_word = if required_blanks == 1 {
581
- "line"
582
- } else {
583
- "lines"
584
- };
585
- issues.push(format!("Expected {} blank {} between headings", required_blanks, line_word));
586
- }
587
- }
588
-
589
- // Check blank lines above
590
- if heading_line > 0 {
591
- let mut blank_lines_above = 0;
598
+ // Efficiently count blank lines above using pre-computed data
599
+ let blank_lines_above = if heading_line > 0 {
600
+ let mut count = 0;
592
601
  for j in (0..heading_line).rev() {
593
- if lines[j].trim().is_empty() {
594
- blank_lines_above += 1;
602
+ if blank_lines[j] {
603
+ count += 1;
595
604
  } else {
596
605
  break;
597
606
  }
598
607
  }
608
+ count
609
+ } else {
610
+ 0
611
+ };
599
612
 
600
- if blank_lines_above < self.lines_above {
601
- let line_word = if self.lines_above == 1 {
602
- "line"
603
- } else {
604
- "lines"
605
- };
606
- issues.push(format!(
607
- "Expected {} blank {} above heading",
608
- self.lines_above, line_word
609
- ));
610
- }
613
+ // Check if we need blank lines above
614
+ if heading_line > 0 && blank_lines_above < self.lines_above {
615
+ let needed_blanks = self.lines_above - blank_lines_above;
616
+ heading_violations.push((heading_line, "above", needed_blanks));
611
617
  }
612
618
 
613
619
  // Check blank lines below
614
620
  let effective_heading_line = heading_line;
615
621
  if effective_heading_line < lines.len() - 1 {
616
- // Special case: Don't require blank lines if the next non-blank line is a code block fence
622
+ // Find next non-blank line efficiently
617
623
  let mut next_non_blank_idx = effective_heading_line + 1;
618
- while next_non_blank_idx < lines.len()
619
- && lines[next_non_blank_idx].trim().is_empty()
620
- {
624
+ while next_non_blank_idx < lines.len() && blank_lines[next_non_blank_idx] {
621
625
  next_non_blank_idx += 1;
622
626
  }
623
627
 
624
- let next_line_is_code_fence = next_non_blank_idx < lines.len() && {
625
- let next_trimmed = lines[next_non_blank_idx].trim();
626
- (next_trimmed.starts_with("```") || next_trimmed.starts_with("~~~"))
627
- && (next_trimmed == "```"
628
- || next_trimmed == "~~~"
629
- || next_trimmed.len() >= 3
630
- && next_trimmed[3..]
631
- .chars()
632
- .next()
633
- .map_or(true, |c| c.is_whitespace() || c.is_alphabetic()))
634
- };
635
-
636
628
  // Check if next line is a code fence or list item
637
- let next_line_is_list = next_non_blank_idx < lines.len() && {
638
- let next_trimmed = lines[next_non_blank_idx].trim();
639
- next_trimmed.starts_with("- ")
640
- || next_trimmed.starts_with("* ")
641
- || next_trimmed.starts_with("+ ")
642
- || next_trimmed
643
- .chars()
644
- .next()
645
- .map_or(false, |c| c.is_ascii_digit())
646
- && next_trimmed.contains(". ")
647
- };
648
-
649
- // If next line is a code fence or list item, we don't need blank lines between
650
- if !next_line_is_code_fence && !next_line_is_list {
651
- let mut blank_lines_below = 0;
652
- for line in lines.iter().skip(effective_heading_line + 1) {
653
- if !line.trim().is_empty() {
654
- break;
655
- }
656
- blank_lines_below += 1;
657
- }
658
-
659
- if blank_lines_below < self.lines_below {
660
- let line_word = if self.lines_below == 1 {
661
- "line"
662
- } else {
663
- "lines"
664
- };
665
- issues.push(format!(
666
- "Expected {} blank {} below heading",
667
- self.lines_below, line_word
668
- ));
669
- }
670
- }
671
- }
672
-
673
- // Generate individual warnings with proper LSP fixes instead of combined warnings
674
- let line_index = LineIndex::new(content.to_string());
675
-
676
- // Check if we need blank lines above
677
- if heading_line > 0 {
678
- let mut blank_lines_above = 0;
679
- for j in (0..heading_line).rev() {
680
- if lines[j].trim().is_empty() {
681
- blank_lines_above += 1;
682
- } else {
683
- break;
684
- }
685
- }
686
-
687
- if blank_lines_above < self.lines_above {
688
- let needed_blanks = self.lines_above - blank_lines_above;
689
- let insertion_point = heading_line + 1; // Insert before the heading line (1-indexed: +1 for 1-indexed)
690
-
691
- // Calculate precise character range for inserting blank lines before heading
692
- let (start_line, start_col, end_line, end_col) =
693
- calculate_heading_range(heading_display_line, line);
694
-
695
- result.push(LintWarning {
696
- rule_name: Some(self.name()),
697
- message: format!(
698
- "Expected {} blank {} above heading",
699
- self.lines_above,
700
- if self.lines_above == 1 { "line" } else { "lines" }
701
- ),
702
- line: start_line,
703
- column: start_col,
704
- end_line,
705
- end_column: end_col,
706
- severity: Severity::Warning,
707
- fix: Some(Fix {
708
- range: line_index.line_col_to_byte_range_with_length(insertion_point, 1, 0),
709
- replacement: "\n".repeat(needed_blanks),
710
- }),
711
- });
712
- }
713
- }
714
-
715
- // Check if we need blank lines below (but skip if next line is list or code fence)
716
- let effective_heading_line = heading_line;
717
- if effective_heading_line < lines.len() - 1 {
718
- // Check what the next non-blank line is
719
- let mut next_non_blank_idx = effective_heading_line + 1;
720
- while next_non_blank_idx < lines.len()
721
- && lines[next_non_blank_idx].trim().is_empty()
722
- {
723
- next_non_blank_idx += 1;
724
- }
725
-
726
- // Skip warning if next line is a code fence or list item (same logic as earlier)
727
- let next_line_is_code_fence = next_non_blank_idx < lines.len() && {
629
+ let next_line_is_special = next_non_blank_idx < lines.len() && {
728
630
  let next_trimmed = lines[next_non_blank_idx].trim();
729
- (next_trimmed.starts_with("```") || next_trimmed.starts_with("~~~"))
631
+ // Code fence check
632
+ ((next_trimmed.starts_with("```") || next_trimmed.starts_with("~~~"))
730
633
  && (next_trimmed == "```"
731
634
  || next_trimmed == "~~~"
732
635
  || next_trimmed.len() >= 3
733
636
  && next_trimmed[3..]
734
637
  .chars()
735
638
  .next()
736
- .map_or(true, |c| c.is_whitespace() || c.is_alphabetic()))
737
- };
738
-
739
- let next_line_is_list = next_non_blank_idx < lines.len() && {
740
- let next_trimmed = lines[next_non_blank_idx].trim();
741
- next_trimmed.starts_with("- ")
742
- || next_trimmed.starts_with("* ")
743
- || next_trimmed.starts_with("+ ")
744
- || next_trimmed
745
- .chars()
746
- .next()
747
- .map_or(false, |c| c.is_ascii_digit())
748
- && next_trimmed.contains(". ")
639
+ .map_or(true, |c| c.is_whitespace() || c.is_alphabetic())))
640
+ // List item check
641
+ || next_trimmed.starts_with("- ")
642
+ || next_trimmed.starts_with("* ")
643
+ || next_trimmed.starts_with("+ ")
644
+ || (next_trimmed
645
+ .chars()
646
+ .next()
647
+ .map_or(false, |c| c.is_ascii_digit())
648
+ && next_trimmed.contains(". "))
749
649
  };
750
650
 
751
651
  // Only generate warning if next line is NOT a code fence or list item
752
- if !next_line_is_code_fence && !next_line_is_list {
753
- let mut blank_lines_below = 0;
754
- for line in lines.iter().skip(effective_heading_line + 1) {
755
- if !line.trim().is_empty() {
756
- break;
757
- }
758
- blank_lines_below += 1;
759
- }
652
+ if !next_line_is_special {
653
+ // Efficiently count blank lines below using pre-computed data
654
+ let blank_lines_below = next_non_blank_idx - effective_heading_line - 1;
760
655
 
761
656
  if blank_lines_below < self.lines_below {
762
657
  let needed_blanks = self.lines_below - blank_lines_below;
763
- let insertion_point = effective_heading_line + 2; // Insert after the heading line (1-indexed)
764
-
765
- // Calculate precise character range for inserting blank lines after heading
766
- let (start_line, start_col, end_line, end_col) =
767
- calculate_heading_range(heading_display_line, line);
768
-
769
- result.push(LintWarning {
770
- rule_name: Some(self.name()),
771
- message: format!(
772
- "Expected {} blank {} below heading",
773
- self.lines_below,
774
- if self.lines_below == 1 { "line" } else { "lines" }
775
- ),
776
- line: start_line,
777
- column: start_col,
778
- end_line,
779
- end_column: end_col,
780
- severity: Severity::Warning,
781
- fix: Some(Fix {
782
- range: line_index.line_col_to_byte_range_with_length(insertion_point, 1, 0),
783
- replacement: "\n".repeat(needed_blanks),
784
- }),
785
- });
658
+ heading_violations.push((heading_line, "below", needed_blanks));
786
659
  }
787
660
  }
788
661
  }
789
-
790
- // Update previous heading index
791
- prev_heading_index = Some(heading_line);
792
662
  }
793
663
  }
794
664
 
665
+ // Generate warnings for all violations
666
+ for (heading_line, position, needed_blanks) in heading_violations {
667
+ let heading_display_line = heading_line + 1; // 1-indexed for display
668
+ let line = lines[heading_line];
669
+
670
+ // Calculate precise character range for the heading
671
+ let (start_line, start_col, end_line, end_col) =
672
+ calculate_heading_range(heading_display_line, line);
673
+
674
+ let (message, insertion_point) = match position {
675
+ "above" => (
676
+ format!(
677
+ "Expected {} blank {} above heading",
678
+ self.lines_above,
679
+ if self.lines_above == 1 { "line" } else { "lines" }
680
+ ),
681
+ heading_line + 1
682
+ ),
683
+ "below" => (
684
+ format!(
685
+ "Expected {} blank {} below heading",
686
+ self.lines_below,
687
+ if self.lines_below == 1 { "line" } else { "lines" }
688
+ ),
689
+ heading_line + 2
690
+ ),
691
+ _ => continue,
692
+ };
693
+
694
+ // Use pre-computed line starts for efficient byte range calculation
695
+ let byte_range = if insertion_point <= lines.len() {
696
+ let start_byte = line_starts[insertion_point - 1]; // Convert 1-indexed to 0-indexed
697
+ start_byte..start_byte
698
+ } else {
699
+ // Insert at end of file
700
+ let last_byte = line_starts[lines.len()];
701
+ last_byte..last_byte
702
+ };
703
+
704
+ result.push(LintWarning {
705
+ rule_name: Some(self.name()),
706
+ message,
707
+ line: start_line,
708
+ column: start_col,
709
+ end_line,
710
+ end_column: end_col,
711
+ severity: Severity::Warning,
712
+ fix: Some(Fix {
713
+ range: byte_range,
714
+ replacement: line_ending.repeat(needed_blanks),
715
+ }),
716
+ });
717
+ }
718
+
795
719
  Ok(result)
796
720
  }
797
721
 
@@ -801,21 +725,11 @@ impl Rule for MD022BlanksAroundHeadings {
801
725
  return Ok(content.to_string());
802
726
  }
803
727
 
804
- // Check if original content ended with newline
805
- let had_trailing_newline = content.ends_with('\n');
806
-
807
728
  // Use a consolidated fix that avoids adding multiple blank lines
808
729
  let lines: Vec<&str> = content.lines().collect();
809
- let fixed = self._fix_content(&lines);
810
-
811
- // Preserve original trailing newline if it existed
812
- let result = if had_trailing_newline && !fixed.ends_with('\n') {
813
- format!("{}\n", fixed)
814
- } else {
815
- fixed
816
- };
730
+ let fixed = self._fix_content(&lines, content);
817
731
 
818
- Ok(result)
732
+ Ok(fixed)
819
733
  }
820
734
 
821
735
  /// Optimized check using document structure
@@ -1334,7 +1248,7 @@ Even more content.
1334
1248
 
1335
1249
  Final content.";
1336
1250
 
1337
- let result = rule._fix_content(&content.lines().collect::<Vec<&str>>());
1251
+ let result = rule._fix_content(&content.lines().collect::<Vec<&str>>(), content);
1338
1252
  assert_eq!(
1339
1253
  result, expected,
1340
1254
  "Fix should only add missing blank lines, never remove existing ones"
@@ -1368,7 +1282,7 @@ Final content.";
1368
1282
 
1369
1283
  let expected = "## Configuration\n\nThis rule has the following configuration options:\n\n- `option1`: Description of option 1.\n- `option2`: Description of option 2.\n\n## Another Section\n\nSome content here.";
1370
1284
 
1371
- let result = rule._fix_content(&content.lines().collect::<Vec<&str>>());
1285
+ let result = rule._fix_content(&content.lines().collect::<Vec<&str>>(), content);
1372
1286
  assert_eq!(
1373
1287
  result, expected,
1374
1288
  "Fix should not add blank lines before lists"
@@ -160,21 +160,68 @@ impl Rule for MD040FencedCodeLanguage {
160
160
  let _line_index = LineIndex::new(content.to_string());
161
161
 
162
162
  let mut result = String::new();
163
-
164
163
  let mut in_code_block = false;
165
-
166
164
  let mut fence_char = None;
165
+ let mut fence_needs_language = false;
166
+ let mut original_indent = String::new();
167
167
 
168
168
  let lines: Vec<&str> = content.lines().collect();
169
- for line in lines.iter() {
169
+
170
+ // Helper function to check if we're in a nested context
171
+ let is_in_nested_context = |line_idx: usize| -> bool {
172
+ // Look for blockquote or list context above this line
173
+ for i in (0..line_idx).rev() {
174
+ let line = lines.get(i).unwrap_or(&"");
175
+ let trimmed = line.trim();
176
+
177
+ // If we hit a blank line, check if context continues
178
+ if trimmed.is_empty() {
179
+ continue;
180
+ }
181
+
182
+ // Check for blockquote markers
183
+ if line.trim_start().starts_with('>') {
184
+ return true;
185
+ }
186
+
187
+ // Check for list markers with sufficient indentation
188
+ if line.len() - line.trim_start().len() >= 2 {
189
+ let after_indent = line.trim_start();
190
+ if after_indent.starts_with("- ") || after_indent.starts_with("* ") ||
191
+ after_indent.starts_with("+ ") ||
192
+ (after_indent.len() > 2 && after_indent.chars().nth(0).unwrap_or(' ').is_ascii_digit() &&
193
+ after_indent.chars().nth(1).unwrap_or(' ') == '.' &&
194
+ after_indent.chars().nth(2).unwrap_or(' ') == ' ') {
195
+ return true;
196
+ }
197
+ }
198
+
199
+ // If we find content that's not indented, we're not in nested context
200
+ if line.starts_with(|c: char| !c.is_whitespace()) {
201
+ break;
202
+ }
203
+ }
204
+ false
205
+ };
206
+
207
+ for (i, line) in lines.iter().enumerate() {
170
208
  let trimmed = line.trim();
171
209
 
172
210
  if let Some(ref current_fence) = fence_char {
173
211
  if trimmed.starts_with(current_fence) {
174
- // This is a closing fence - use no indentation
175
- result.push_str(&format!("{}\n", current_fence));
212
+ // This is a closing fence
213
+ if fence_needs_language {
214
+ // Use the same indentation as the opening fence
215
+ result.push_str(&format!("{}{}\n", original_indent, trimmed));
216
+ } else {
217
+ // Preserve original line as-is
218
+ result.push_str(line);
219
+ result.push('\n');
220
+ }
176
221
  in_code_block = false;
177
222
  fence_char = None;
223
+ fence_needs_language = false;
224
+ original_indent.clear();
178
225
  continue;
179
226
  }
180
227
 
@@ -190,15 +237,30 @@ impl Rule for MD040FencedCodeLanguage {
190
237
  };
191
238
  fence_char = Some(fence.to_string());
192
239
 
240
+ // Capture the original indentation
241
+ let line_indent = line[..line.len() - line.trim_start().len()].to_string();
242
+
193
243
  // Add 'text' as default language for opening fence if no language specified
194
244
  let after_fence = trimmed[fence.len()..].trim();
195
245
  if after_fence.is_empty() {
196
- // Use no indentation for the opening fence with language
197
- result.push_str(&format!("{}text\n", fence));
246
+ // Decide whether to preserve indentation based on context
247
+ let should_preserve_indent = is_in_nested_context(i);
248
+
249
+ if should_preserve_indent {
250
+ // Preserve indentation for nested contexts
251
+ original_indent = line_indent;
252
+ result.push_str(&format!("{}{}text\n", original_indent, fence));
253
+ } else {
254
+ // Remove indentation for standalone code blocks
255
+ original_indent = String::new();
256
+ result.push_str(&format!("{}text\n", fence));
257
+ }
258
+ fence_needs_language = true;
198
259
  } else {
199
- // Keep original indentation for fences that already have a language
260
+ // Keep original line as-is since it already has a language
200
261
  result.push_str(line);
201
262
  result.push('\n');
263
+ fence_needs_language = false;
202
264
  }
203
265
  } else {
204
266
  result.push_str(line);