use crate::lint::rule::Rule;
use crate::markdown::MarkdownParser;
use crate::types::{Fix, Violation};
use pulldown_cmark::{Event, Tag};
use serde_json::Value;
pub struct MD022;
impl Rule for MD022 {
fn name(&self) -> &str {
"MD022"
}
fn description(&self) -> &str {
"Headings should be surrounded by blank lines"
}
fn tags(&self) -> &[&str] {
&["headings", "headers", "blank_lines"]
}
fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
let mut violations = Vec::new();
let lines = parser.lines();
let mut heading_lines = Vec::new();
for (event, range) in parser.parse_with_offsets() {
if let Event::Start(Tag::Heading(_, _, _)) = event {
let line = parser.offset_to_line(range.start);
heading_lines.push(line);
}
}
for &heading_line in &heading_lines {
let line_idx = heading_line - 1;
if line_idx > 0 {
let prev_line = lines[line_idx - 1].trim();
if !prev_line.is_empty() {
violations.push(Violation {
line: heading_line,
column: Some(1),
rule: self.name().to_string(),
message: "Heading should be surrounded by blank lines (missing before)"
.to_string(),
fix: Some(Fix {
line_start: line_idx,
line_end: line_idx,
column_start: None,
column_end: None,
replacement: format!("\n{}", lines[line_idx]),
description: "Add blank line before heading".to_string(),
}),
});
}
}
if line_idx + 1 < lines.len() {
let next_line = lines[line_idx + 1].trim();
if !next_line.is_empty()
&& !next_line.starts_with('#')
&& !next_line
.chars()
.all(|c| c == '=' || c == '-' || c.is_whitespace())
{
violations.push(Violation {
line: heading_line,
column: Some(1),
rule: self.name().to_string(),
message: "Heading should be surrounded by blank lines (missing after)"
.to_string(),
fix: Some(Fix {
line_start: line_idx + 1,
line_end: line_idx + 1,
column_start: None,
column_end: None,
replacement: format!("{}\n", lines[line_idx]),
description: "Add blank line after heading".to_string(),
}),
});
}
}
}
violations
}
fn fixable(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_properly_surrounded() {
let content = "Paragraph\n\n# Heading\n\nAnother paragraph";
let parser = MarkdownParser::new(content);
let rule = MD022;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
#[test]
fn test_missing_blank_before() {
let content = "Paragraph\n# Heading\n\nContent";
let parser = MarkdownParser::new(content);
let rule = MD022;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("before"));
}
#[test]
fn test_missing_blank_after() {
let content = "\n# Heading\nContent";
let parser = MarkdownParser::new(content);
let rule = MD022;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("after"));
}
#[test]
fn test_first_line() {
let content = "# Heading\n\nContent";
let parser = MarkdownParser::new(content);
let rule = MD022;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0); }
}