use crate::error::Result;
use crate::rule::{AstRule, RuleCategory, RuleMetadata};
use crate::{
Document,
violation::{Severity, Violation},
};
pub struct MD009 {
br_spaces: usize,
list_item_empty_lines: bool,
strict: bool,
}
impl MD009 {
pub fn new() -> Self {
Self {
br_spaces: 2, list_item_empty_lines: false,
strict: false,
}
}
#[allow(dead_code)]
pub fn with_config(br_spaces: usize, list_item_empty_lines: bool, strict: bool) -> Self {
Self {
br_spaces,
list_item_empty_lines,
strict,
}
}
}
impl Default for MD009 {
fn default() -> Self {
Self::new()
}
}
impl AstRule for MD009 {
fn id(&self) -> &'static str {
"MD009"
}
fn name(&self) -> &'static str {
"no-trailing-spaces"
}
fn description(&self) -> &'static str {
"Trailing spaces are not allowed"
}
fn metadata(&self) -> RuleMetadata {
RuleMetadata::stable(RuleCategory::Formatting).introduced_in("markdownlint v0.1.0")
}
fn check_ast<'a>(
&self,
document: &Document,
ast: &'a comrak::nodes::AstNode<'a>,
) -> Result<Vec<Violation>> {
let mut violations = Vec::new();
let code_block_lines = self.get_code_block_line_ranges(ast);
let list_item_lines = if self.list_item_empty_lines {
self.get_list_item_empty_lines(ast)
} else {
Vec::new()
};
for (line_number, line) in document.lines.iter().enumerate() {
let line_num = line_number + 1;
if !line.ends_with(' ') && !line.ends_with('\t') {
continue;
}
let trailing_spaces = line.chars().rev().take_while(|c| c.is_whitespace()).count();
let in_code_block = code_block_lines
.iter()
.any(|(start, end)| line_num >= *start && line_num <= *end);
if in_code_block && !self.strict {
continue;
}
if self.list_item_empty_lines && list_item_lines.contains(&line_num) {
continue;
}
if !self.strict && trailing_spaces == self.br_spaces {
continue;
}
let column = line.len() - trailing_spaces + 1;
violations.push(self.create_violation(
format!(
"Trailing spaces detected (found {} trailing space{})",
trailing_spaces,
if trailing_spaces == 1 { "" } else { "s" }
),
line_num,
column,
Severity::Warning,
));
}
Ok(violations)
}
}
impl MD009 {
fn get_code_block_line_ranges<'a>(
&self,
ast: &'a comrak::nodes::AstNode<'a>,
) -> Vec<(usize, usize)> {
let mut ranges = Vec::new();
self.collect_code_block_ranges(ast, &mut ranges);
ranges
}
#[allow(clippy::only_used_in_recursion)]
fn collect_code_block_ranges<'a>(
&self,
node: &'a comrak::nodes::AstNode<'a>,
ranges: &mut Vec<(usize, usize)>,
) {
use comrak::nodes::NodeValue;
if let NodeValue::CodeBlock(_) = &node.data.borrow().value {
let sourcepos = node.data.borrow().sourcepos;
if sourcepos.start.line > 0 && sourcepos.end.line > 0 {
ranges.push((sourcepos.start.line, sourcepos.end.line));
}
}
for child in node.children() {
self.collect_code_block_ranges(child, ranges);
}
}
fn get_list_item_empty_lines<'a>(&self, ast: &'a comrak::nodes::AstNode<'a>) -> Vec<usize> {
let mut lines = Vec::new();
self.collect_list_item_empty_lines(ast, &mut lines);
lines
}
#[allow(clippy::only_used_in_recursion)]
fn collect_list_item_empty_lines<'a>(
&self,
node: &'a comrak::nodes::AstNode<'a>,
lines: &mut Vec<usize>,
) {
use comrak::nodes::NodeValue;
if let NodeValue::Item(_) = &node.data.borrow().value {
}
for child in node.children() {
self.collect_list_item_empty_lines(child, lines);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rule::Rule;
use std::path::PathBuf;
fn create_test_document(content: &str) -> Document {
Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
}
#[test]
fn test_md009_no_trailing_spaces() {
let content = "# Heading\n\nNo trailing spaces here.\nAnother clean line.";
let document = create_test_document(content);
let rule = MD009::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md009_single_trailing_space() {
let content = "# Heading\n\nLine with single trailing space. \nClean line.";
let document = create_test_document(content);
let rule = MD009::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule_id, "MD009");
assert_eq!(violations[0].line, 3);
assert_eq!(violations[0].column, 33);
assert!(violations[0].message.contains("1 trailing space"));
}
#[test]
fn test_md009_multiple_trailing_spaces() {
let content = "# Heading\n\nLine with spaces. \nAnother line. ";
let document = create_test_document(content);
let rule = MD009::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 2);
assert_eq!(violations[0].line, 3);
assert!(violations[0].message.contains("3 trailing spaces"));
assert_eq!(violations[1].line, 4);
assert!(violations[1].message.contains("4 trailing spaces"));
}
#[test]
fn test_md009_trailing_tabs() {
let content = "# Heading\n\nLine with trailing tab.\t\nClean line.";
let document = create_test_document(content);
let rule = MD009::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].line, 3);
assert!(violations[0].message.contains("1 trailing space"));
}
#[test]
fn test_md009_line_break_spaces() {
let content = "# Heading\n\nLine with two spaces for break. \nNext line.";
let document = create_test_document(content);
let rule = MD009::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md009_strict_mode() {
let content = "# Heading\n\nLine with two spaces. \nThree spaces. ";
let document = create_test_document(content);
let rule = MD009::with_config(2, false, true);
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 2);
}
#[test]
fn test_md009_code_block_ignored() {
let content = "# Heading\n\n```rust\nlet x = 1; \n```\n\nRegular line. ";
let document = create_test_document(content);
let rule = MD009::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].line, 7);
}
#[test]
fn test_md009_code_block_strict() {
let content = "# Heading\n\n```rust\nlet x = 1; \n```\n\nRegular line. ";
let document = create_test_document(content);
let rule = MD009::with_config(2, false, true);
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 2);
}
}