use crate::lint::rule::Rule;
use crate::markdown::MarkdownParser;
use crate::types::{Fix, Violation};
use serde_json::Value;
pub struct MD020;
impl Rule for MD020 {
fn name(&self) -> &str {
"MD020"
}
fn description(&self) -> &str {
"No space inside hashes on closed atx style heading"
}
fn tags(&self) -> &[&str] {
&["headings", "atx_closed", "spaces"]
}
fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
let mut violations = Vec::new();
for (line_num, line) in parser.lines().iter().enumerate() {
let line_number = line_num + 1;
let trimmed = line.trim();
if trimmed.starts_with('#') && trimmed.ends_with('#') {
let opening_hashes = trimmed.chars().take_while(|&c| c == '#').count();
let closing_hashes = trimmed.chars().rev().take_while(|&c| c == '#').count();
if opening_hashes + closing_hashes < trimmed.len() {
let chars: Vec<char> = trimmed.chars().collect();
let pos_before_closing = chars.len() - closing_hashes - 1;
if chars[pos_before_closing] != ' ' {
let before_closing: String = chars[..=pos_before_closing].iter().collect();
let closing: String = chars[(pos_before_closing + 1)..].iter().collect();
let replacement = format!("{} {}", before_closing, closing);
violations.push(Violation {
line: line_number,
column: Some(1),
rule: self.name().to_string(),
message: "No space inside hashes on closed atx style heading"
.to_string(),
fix: Some(Fix {
line_start: line_number,
line_end: line_number,
column_start: None,
column_end: None,
replacement,
description: "Add space before closing hashes".to_string(),
}),
});
}
}
}
}
violations
}
fn fixable(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_correct_closed_heading() {
let content = "# Heading #";
let parser = MarkdownParser::new(content);
let rule = MD020;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0); }
#[test]
fn test_no_space_before_closing() {
let content = "# Heading#";
let parser = MarkdownParser::new(content);
let rule = MD020;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 1); }
#[test]
fn test_regular_heading() {
let content = "# Heading";
let parser = MarkdownParser::new(content);
let rule = MD020;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0); }
#[test]
fn test_multiple_levels() {
let content = "## Heading##\n### Another###";
let parser = MarkdownParser::new(content);
let rule = MD020;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 2); }
}