use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule};
#[derive(Debug)]
pub struct MD009TrailingSpaces {
pub br_spaces: usize,
pub strict: bool,
}
impl Default for MD009TrailingSpaces {
fn default() -> Self {
Self {
br_spaces: 2,
strict: false,
}
}
}
impl MD009TrailingSpaces {
pub fn new(br_spaces: usize, strict: bool) -> Self {
Self { br_spaces, strict }
}
fn is_in_code_block(lines: &[&str], current_line: usize) -> bool {
let mut fence_count = 0;
for (i, line) in lines.iter().take(current_line + 1).enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
fence_count += 1;
}
if i == current_line && fence_count % 2 == 1 {
return true;
}
}
false
}
fn is_empty_blockquote_line(line: &str) -> bool {
let trimmed = line.trim_start();
trimmed.starts_with('>') && trimmed.trim_end() == ">"
}
fn count_trailing_spaces(line: &str) -> usize {
let mut count = 0;
for c in line.chars().rev() {
if c == ' ' {
count += 1;
} else {
break;
}
}
count
}
}
impl Rule for MD009TrailingSpaces {
fn name(&self) -> &'static str {
"MD009"
}
fn description(&self) -> &'static str {
"Trailing spaces should be removed"
}
fn check(&self, content: &str) -> LintResult {
let mut warnings = Vec::new();
let lines: Vec<&str> = content.lines().collect();
for (line_num, &line) in lines.iter().enumerate() {
let trailing_spaces = Self::count_trailing_spaces(line);
if trailing_spaces == 0 {
continue;
}
if line.trim().is_empty() {
if trailing_spaces > 0 {
warnings.push(LintWarning {
line: line_num + 1,
column: 1,
message: "Empty line should not have trailing spaces".to_string(),
fix: Some(Fix {
line: line_num + 1,
column: 1,
replacement: String::new(),
}),
});
}
continue;
}
if !self.strict && Self::is_in_code_block(&lines, line_num) {
continue;
}
if !self.strict && trailing_spaces == self.br_spaces {
continue;
}
if Self::is_empty_blockquote_line(line) {
let trimmed = line.trim_end();
warnings.push(LintWarning {
line: line_num + 1,
column: trimmed.len() + 1,
message: "Empty blockquote line should have a space after >".to_string(),
fix: Some(Fix {
line: line_num + 1,
column: trimmed.len() + 1,
replacement: format!("{} ", trimmed),
}),
});
continue;
}
let trimmed = line.trim_end();
warnings.push(LintWarning {
line: line_num + 1,
column: trimmed.len() + 1,
message: if trailing_spaces == 1 {
"Trailing space found".to_string()
} else {
format!("{} trailing spaces found", trailing_spaces)
},
fix: Some(Fix {
line: line_num + 1,
column: trimmed.len() + 1,
replacement: if !self.strict && line_num < lines.len() - 1 {
format!("{}{}", trimmed, " ".repeat(self.br_spaces))
} else {
trimmed.to_string()
},
}),
});
}
Ok(warnings)
}
fn fix(&self, content: &str) -> Result<String, LintError> {
let mut result = String::new();
let lines: Vec<&str> = content.lines().collect();
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim_end();
if trimmed.is_empty() {
result.push('\n');
continue;
}
if !self.strict && Self::is_in_code_block(&lines, i) {
result.push_str(line);
result.push('\n');
continue;
}
if Self::is_empty_blockquote_line(line) {
result.push_str(trimmed);
result.push(' '); result.push('\n');
continue;
}
if !self.strict && i < lines.len() - 1 && Self::count_trailing_spaces(line) >= 1 {
result.push_str(trimmed);
result.push_str(&" ".repeat(self.br_spaces));
} else {
result.push_str(trimmed);
}
result.push('\n');
}
if !content.ends_with('\n') {
result.pop();
}
Ok(result)
}
}