use crate::lint::rule::Rule;
use crate::markdown::MarkdownParser;
use crate::types::{Fix, Violation};
use serde_json::Value;
pub struct MD047;
impl Rule for MD047 {
fn name(&self) -> &str {
"MD047"
}
fn description(&self) -> &str {
"Files should end with a single newline character"
}
fn tags(&self) -> &[&str] {
&["blank_lines"]
}
fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
let mut violations = Vec::new();
let content = parser.content();
if content.is_empty() {
return violations;
}
let lines = parser.lines();
if !content.ends_with('\n') {
let last_line = lines.last().unwrap_or(&"");
violations.push(Violation {
line: lines.len(),
column: Some(1),
rule: self.name().to_string(),
message: "Files should end with a single newline character".to_string(),
fix: Some(Fix {
line_start: lines.len(),
line_end: lines.len(),
column_start: None,
column_end: None,
replacement: format!("{}\n", last_line),
description: "Add newline at end of file".to_string(),
}),
});
} else if content.ends_with("\n\n") {
let trailing_newlines = content.chars().rev().take_while(|&c| c == '\n').count();
if trailing_newlines > 1 {
let last_content_line_idx = lines.len().saturating_sub(trailing_newlines);
let last_content_line = if last_content_line_idx > 0 {
lines.get(last_content_line_idx - 1).unwrap_or(&"")
} else {
""
};
violations.push(Violation {
line: lines.len(),
column: Some(1),
rule: self.name().to_string(),
message: "Files should end with a single newline character".to_string(),
fix: Some(Fix {
line_start: last_content_line_idx.max(1),
line_end: lines.len(),
column_start: None,
column_end: None,
replacement: if last_content_line_idx > 0 {
format!("{}\n", last_content_line)
} else {
"\n".to_string()
},
description: "Remove extra newlines at end of file".to_string(),
}),
});
}
}
violations
}
fn fixable(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_newline() {
let content = "# Heading\n\nContent\n";
let parser = MarkdownParser::new(content);
let rule = MD047;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
#[test]
fn test_no_newline() {
let content = "# Heading\n\nContent";
let parser = MarkdownParser::new(content);
let rule = MD047;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 1);
}
#[test]
fn test_multiple_newlines() {
let content = "# Heading\n\nContent\n\n";
let parser = MarkdownParser::new(content);
let rule = MD047;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 1);
}
#[test]
fn test_empty_file() {
let content = "";
let parser = MarkdownParser::new(content);
let rule = MD047;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0); }
}