Skip to content

Commit 27120f2

Browse files
committed
fix(md022): require a blank line below a heading above a spaced thematic break
The syntactic list test that keeps MD022 from demanding a blank line around a list item also matched breaks written as a marker, a space and more markers: `* * *`, `- - -`, `- --`. Every other spelling (`***`, `---`, `___`, `_ _ _`, `---- ----`) was reported, so whether a heading above a thematic break got its blank line depended on how the break was written, even though rumdl parses all of them the same way. The test now excludes whatever `is_horizontal_rule_content` recognizes, which is what the per-line `is_horizontal_rule` flag is computed from, so it matches the parser instead of being stricter than it. It still reads the line text alone, which is what keeps the blank-below fix idempotent.
1 parent 2d60067 commit 27120f2

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

‎src/rules/md022_blanks_around_headings.rs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::lint_context::is_horizontal_rule_content;
12
/// Rule MD022: Headings should be surrounded by blank lines
23
///
34
/// See [docs/md022.md](../../docs/md022.md) for full documentation, configuration, and examples.
@@ -17,7 +18,17 @@ use md022_config::MD022Config;
1718
/// a heading is followed by a list. The flag depends on surrounding block context and can flip
1819
/// when a blank line is inserted above the heading, which would make the blank-below fix
1920
/// non-idempotent; the syntactic shape of the following line does not change.
21+
///
22+
/// A spaced thematic break (`* * *`, `- - -`, `- --`) opens with a marker and a space, so it has
23+
/// to be excluded explicitly: the parser reads it as a thematic break rather than a list, and a
24+
/// heading above it needs the same blank line that `***` and `_ _ _` already get.
25+
/// `is_horizontal_rule_content` is what the per-line `is_horizontal_rule` flag is computed from,
26+
/// so the exclusion matches how the rest of rumdl recognizes a break, and it reads the line text
27+
/// alone, which keeps it as parse-stable as the rest of this test.
2028
fn starts_with_list_marker(trimmed: &str) -> bool {
29+
if is_horizontal_rule_content(trimmed) {
30+
return false;
31+
}
2132
let bytes = trimmed.as_bytes();
2233
match bytes.first() {
2334
Some(b'-' | b'*' | b'+') => matches!(bytes.get(1), None | Some(b' ')),
@@ -986,6 +997,58 @@ Final content.";
986997
}
987998
}
988999

1000+
#[test]
1001+
fn test_thematic_break_below_heading_is_not_a_list_item() {
1002+
// A break written with a marker and a space (`* * *`, `- - -`, `- --`) opens like a
1003+
// list item, so the syntactic list test used to exempt it from the blank-below
1004+
// requirement while every other spelling was reported. `---- ----` shows the
1005+
// inconsistency from the other side: it was already reported, because its second
1006+
// character is not a space. rumdl parses all of these as thematic breaks (MD032
1007+
// sees no list), so they must behave identically.
1008+
let rule = MD022BlanksAroundHeadings::default();
1009+
for marker in [
1010+
"* * *",
1011+
"- - -",
1012+
"_ _ _",
1013+
"***",
1014+
"---",
1015+
"___",
1016+
"- --",
1017+
"* ** *",
1018+
"---- ----",
1019+
] {
1020+
let content = format!("text\n\n# Heading\n{marker}\nafter\n");
1021+
let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
1022+
let result = rule.check(&ctx).unwrap();
1023+
assert_eq!(
1024+
result.len(),
1025+
1,
1026+
"a heading above `{marker}` needs a blank line below it, got {result:?}"
1027+
);
1028+
assert_eq!(
1029+
rule.fix(&ctx).unwrap(),
1030+
format!("text\n\n# Heading\n\n{marker}\nafter\n"),
1031+
"fix must insert the blank line below the heading for `{marker}`"
1032+
);
1033+
}
1034+
}
1035+
1036+
#[test]
1037+
fn test_list_item_below_heading_is_still_exempt() {
1038+
// The control for the exclusion above: real list items, including `+ + +`, which is
1039+
// a list item and not a thematic break because `+` is not a thematic break marker.
1040+
let rule = MD022BlanksAroundHeadings::default();
1041+
for item in ["- item", "* item", "+ item", "1. item", "+ + +"] {
1042+
let content = format!("text\n\n# Heading\n{item}\n");
1043+
let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
1044+
assert!(
1045+
rule.check(&ctx).unwrap().is_empty(),
1046+
"a list below a heading stays exempt, but `{item}` was reported"
1047+
);
1048+
assert_eq!(rule.fix(&ctx).unwrap(), content, "`{item}` must not be rewritten");
1049+
}
1050+
}
1051+
9891052
#[test]
9901053
fn test_per_level_configuration_no_blank_above_h1() {
9911054
use md022_config::HeadingLevelConfig;

0 commit comments

Comments
 (0)