use crate::lint::rule::Rule;
use crate::markdown::MarkdownParser;
use crate::types::{Fix, Violation};
use serde_json::Value;
pub struct MD019;
impl Rule for MD019 {
fn name(&self) -> &str {
"MD019"
}
fn description(&self) -> &str {
"Multiple spaces after hash on atx style heading"
}
fn tags(&self) -> &[&str] {
&["headings", "headers", "atx", "spaces"]
}
fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
let mut violations = Vec::new();
let code_block_lines = parser.get_code_block_line_numbers();
for (line_num, line) in parser.lines().iter().enumerate() {
let line_number = line_num + 1;
if code_block_lines.contains(&line_number) {
continue;
}
let trimmed = line.trim();
if trimmed.starts_with('#') {
let hash_count = trimmed.chars().take_while(|&c| c == '#').count();
if hash_count > 0 && hash_count <= 6 && trimmed.len() > hash_count {
let after_hashes = &trimmed[hash_count..];
let space_count = after_hashes.chars().take_while(|&c| c == ' ').count();
if space_count > 1 {
let hashes = "#".repeat(hash_count);
let rest = after_hashes[space_count..].trim_start();
let replacement = format!("{} {}", hashes, rest);
violations.push(Violation {
line: line_number,
column: Some(hash_count + 2),
rule: self.name().to_string(),
message: format!(
"Multiple spaces after hash on atx style heading ({} spaces)",
space_count
),
fix: Some(Fix {
line_start: line_number,
line_end: line_number,
column_start: None,
column_end: None,
replacement,
description: "Replace multiple spaces with single space"
.to_string(),
}),
});
}
}
}
}
violations
}
fn fixable(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fix::Fixer;
fn apply_fixes(content: &str, violations: &[Violation]) -> String {
let fixes: Vec<_> = violations.iter().filter_map(|v| v.fix.clone()).collect();
Fixer::new()
.apply_fixes_to_content(content, &fixes)
.unwrap()
}
#[test]
fn test_correct_single_space() {
let content = "# Heading 1\n## Heading 2\n### Heading 3";
let parser = MarkdownParser::new(content);
let rule = MD019;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
#[test]
fn test_multiple_spaces() {
let content = "# Heading with 2 spaces\n## Correct heading";
let parser = MarkdownParser::new(content);
let rule = MD019;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].line, 1);
}
#[test]
fn test_many_spaces() {
let content = "### Heading with 5 spaces";
let parser = MarkdownParser::new(content);
let rule = MD019;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("5 spaces"));
}
#[test]
fn test_heading_in_code_block_not_flagged() {
let content = "# Real heading\n\n```\n## WouldBeViolation\n```\n";
let parser = MarkdownParser::new(content);
let rule = MD019;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
#[test]
fn test_fix_collapses_multiple_spaces() {
let content = "# Too many spaces\n\n### Even more\n";
let parser = MarkdownParser::new(content);
let rule = MD019;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 2);
let fixed = apply_fixes(content, &violations);
assert_eq!(fixed, "# Too many spaces\n\n### Even more\n");
}
}