use mdbook_lint_core::error::Result;
use mdbook_lint_core::rule::{Rule, RuleCategory, RuleMetadata};
use mdbook_lint_core::{
Document,
violation::{Fix, Position, Severity, Violation},
};
pub struct MD012 {
maximum: usize,
}
impl MD012 {
pub fn new() -> Self {
Self { maximum: 1 }
}
#[allow(dead_code)]
pub fn with_maximum(maximum: usize) -> Self {
Self { maximum }
}
pub fn from_config(config: &toml::Value) -> Self {
let mut rule = Self::new();
if let Some(maximum) = config.get("maximum").and_then(|v| v.as_integer()) {
rule.maximum = maximum as usize;
}
rule
}
}
impl Default for MD012 {
fn default() -> Self {
Self::new()
}
}
impl Rule for MD012 {
fn id(&self) -> &'static str {
"MD012"
}
fn name(&self) -> &'static str {
"no-multiple-blanks"
}
fn description(&self) -> &'static str {
"Multiple consecutive blank lines are not allowed"
}
fn metadata(&self) -> RuleMetadata {
RuleMetadata::stable(RuleCategory::Formatting).introduced_in("markdownlint v0.1.0")
}
fn can_fix(&self) -> bool {
true
}
fn check_with_ast<'a>(
&self,
document: &Document,
_ast: Option<&'a comrak::nodes::AstNode<'a>>,
) -> Result<Vec<Violation>> {
let mut violations = Vec::new();
let mut consecutive_blank_lines = 0;
let mut blank_sequence_start = 0;
for (line_number, line) in document.lines.iter().enumerate() {
let line_num = line_number + 1;
if line.trim().is_empty() {
if consecutive_blank_lines == 0 {
blank_sequence_start = line_num;
}
consecutive_blank_lines += 1;
} else {
if consecutive_blank_lines > self.maximum {
let extra_lines = consecutive_blank_lines - self.maximum;
let fix_start_line = blank_sequence_start + self.maximum;
let fix_end_line = blank_sequence_start + consecutive_blank_lines - 1;
let fix = Fix {
description: format!(
"Remove {} extra blank line{}",
extra_lines,
if extra_lines == 1 { "" } else { "s" }
),
replacement: Some(String::new()), start: Position {
line: fix_start_line,
column: 1,
},
end: Position {
line: fix_end_line + 1, column: 1,
},
};
violations.push(self.create_violation_with_fix(
format!(
"Multiple consecutive blank lines ({} found, {} allowed)",
consecutive_blank_lines, self.maximum
),
blank_sequence_start + self.maximum, 1,
Severity::Warning,
fix,
));
}
consecutive_blank_lines = 0;
}
}
if consecutive_blank_lines > self.maximum {
let extra_lines = consecutive_blank_lines - self.maximum;
let fix_start_line = blank_sequence_start + self.maximum;
let fix_end_line = blank_sequence_start + consecutive_blank_lines - 1;
let fix = Fix {
description: format!(
"Remove {} extra blank line{} at end of file",
extra_lines,
if extra_lines == 1 { "" } else { "s" }
),
replacement: Some(String::new()), start: Position {
line: fix_start_line,
column: 1,
},
end: Position {
line: fix_end_line + 1,
column: 1,
},
};
violations.push(self.create_violation_with_fix(
format!(
"Multiple consecutive blank lines at end of file ({} found, {} allowed)",
consecutive_blank_lines, self.maximum
),
blank_sequence_start + self.maximum,
1,
Severity::Warning,
fix,
));
}
Ok(violations)
}
}
#[cfg(test)]
mod tests {
use super::*;
use mdbook_lint_core::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_md012_no_consecutive_blank_lines() {
let content = "# Heading\n\nParagraph one.\n\nParagraph two.";
let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md012_two_consecutive_blank_lines() {
let content = "# Heading\n\n\nParagraph.";
let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule_id, "MD012");
assert_eq!(violations[0].line, 3); assert!(violations[0].message.contains("2 found, 1 allowed"));
}
#[test]
fn test_md012_three_consecutive_blank_lines() {
let content = "# Heading\n\n\n\nParagraph.";
let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].line, 3); assert!(violations[0].message.contains("3 found, 1 allowed"));
}
#[test]
fn test_md012_multiple_violations() {
let content = "# Heading\n\n\nParagraph.\n\n\n\nAnother paragraph.";
let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 2);
assert_eq!(violations[0].line, 3);
assert_eq!(violations[1].line, 6);
}
#[test]
fn test_md012_custom_maximum() {
let content = "# Heading\n\n\nParagraph.";
let document = create_test_document(content);
let rule = MD012::with_maximum(2);
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md012_custom_maximum_violation() {
let content = "# Heading\n\n\n\nParagraph.";
let document = create_test_document(content);
let rule = MD012::with_maximum(2);
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("3 found, 2 allowed"));
}
#[test]
fn test_md012_blank_lines_at_end() {
let content = "# Heading\n\nParagraph.\n\n\n";
let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("at end of file"));
}
#[test]
fn test_md012_zero_maximum() {
let content = "# Heading\n\nParagraph.";
let document = create_test_document(content);
let rule = MD012::with_maximum(0);
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("1 found, 0 allowed"));
}
#[test]
fn test_md012_only_blank_lines() {
let content = "\n\n\n";
let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("at end of file"));
}
#[test]
fn test_md012_fix_two_consecutive_blanks() {
let content = "# Heading\n\n\nParagraph.";
let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].fix.is_some());
let fix = violations[0].fix.as_ref().unwrap();
assert_eq!(fix.replacement.as_ref().unwrap(), ""); assert_eq!(fix.description, "Remove 1 extra blank line");
assert_eq!(fix.start.line, 3); assert_eq!(fix.end.line, 4); }
#[test]
fn test_md012_fix_three_consecutive_blanks() {
let content = "# Heading\n\n\n\nParagraph.";
let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].fix.is_some());
let fix = violations[0].fix.as_ref().unwrap();
assert_eq!(fix.replacement.as_ref().unwrap(), ""); assert_eq!(fix.description, "Remove 2 extra blank lines");
assert_eq!(fix.start.line, 3); assert_eq!(fix.end.line, 5); }
#[test]
fn test_md012_fix_blanks_at_end() {
let content = "# Heading\n\nParagraph.\n\n\n";
let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].fix.is_some());
let fix = violations[0].fix.as_ref().unwrap();
assert_eq!(fix.replacement.as_ref().unwrap(), "");
assert_eq!(fix.description, "Remove 1 extra blank line at end of file");
}
#[test]
fn test_md012_fix_multiple_violations() {
let content = "# Heading\n\n\nFirst paragraph.\n\n\n\nSecond paragraph.";
let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 2);
assert!(violations[0].fix.is_some());
let fix1 = violations[0].fix.as_ref().unwrap();
assert_eq!(fix1.description, "Remove 1 extra blank line");
assert_eq!(fix1.start.line, 3);
assert_eq!(fix1.end.line, 4);
assert!(violations[1].fix.is_some());
let fix2 = violations[1].fix.as_ref().unwrap();
assert_eq!(fix2.description, "Remove 2 extra blank lines");
assert_eq!(fix2.start.line, 6);
assert_eq!(fix2.end.line, 8);
}
#[test]
fn test_md012_fix_with_custom_maximum() {
let content = "# Heading\n\n\n\nParagraph.";
let document = create_test_document(content);
let rule = MD012::with_maximum(2); let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].fix.is_some());
let fix = violations[0].fix.as_ref().unwrap();
assert_eq!(fix.description, "Remove 1 extra blank line");
assert_eq!(fix.start.line, 4); assert_eq!(fix.end.line, 5);
}
#[test]
fn test_md012_fix_zero_maximum() {
let content = "# Heading\n\nParagraph.";
let document = create_test_document(content);
let rule = MD012::with_maximum(0); let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].fix.is_some());
let fix = violations[0].fix.as_ref().unwrap();
assert_eq!(fix.description, "Remove 1 extra blank line");
assert_eq!(fix.start.line, 2); assert_eq!(fix.end.line, 3);
}
#[test]
fn test_md012_fix_many_consecutive_blanks() {
let content = "Start\n\n\n\n\n\n\nEnd"; let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].fix.is_some());
let fix = violations[0].fix.as_ref().unwrap();
assert_eq!(fix.description, "Remove 5 extra blank lines");
assert_eq!(fix.start.line, 3); assert_eq!(fix.end.line, 8); }
#[test]
fn test_md012_fix_position_accuracy() {
let content = "Line 1\n\n\nLine 4";
let document = create_test_document(content);
let rule = MD012::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
let fix = violations[0].fix.as_ref().unwrap();
assert_eq!(fix.start.column, 1);
assert_eq!(fix.end.column, 1);
assert!(violations[0].message.contains("2 found, 1 allowed"));
}
}