use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule};
#[derive(Debug)]
pub struct MD012NoMultipleBlanks {
pub maximum: usize,
}
impl Default for MD012NoMultipleBlanks {
fn default() -> Self {
Self { maximum: 1 }
}
}
impl MD012NoMultipleBlanks {
pub fn new(maximum: usize) -> Self {
Self { maximum }
}
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() {
if line.trim_start().starts_with("```") || line.trim_start().starts_with("~~~") {
fence_count += 1;
}
if i == current_line && fence_count % 2 == 1 {
return true;
}
}
false
}
fn is_in_front_matter(lines: &[&str], current_line: usize) -> bool {
if current_line == 0 {
return lines[0].trim() == "---";
}
let mut dashes = 0;
for (i, line) in lines.iter().take(current_line + 1).enumerate() {
if line.trim() == "---" {
dashes += 1;
}
if i == current_line && dashes == 1 {
return true;
}
}
false
}
}
impl Rule for MD012NoMultipleBlanks {
fn name(&self) -> &'static str {
"MD012"
}
fn description(&self) -> &'static str {
"Multiple consecutive blank lines"
}
fn check(&self, content: &str) -> LintResult {
let mut warnings = Vec::new();
let mut blank_count = 0;
let mut blank_start = 0;
let lines: Vec<&str> = content.lines().collect();
for (line_num, &line) in lines.iter().enumerate() {
if Self::is_in_code_block(&lines, line_num) || Self::is_in_front_matter(&lines, line_num) {
continue;
}
if line.trim().is_empty() {
if blank_count == 0 {
blank_start = line_num;
}
blank_count += 1;
} else {
if blank_count > self.maximum {
let location = if blank_start == 0 {
"at start of file"
} else {
"between content"
};
warnings.push(LintWarning {
message: format!(
"Multiple consecutive blank lines {} ({} > {})",
location, blank_count, self.maximum
),
line: blank_start + 1,
column: 1,
fix: Some(Fix {
line: blank_start + 1,
column: 1,
replacement: "\n".repeat(self.maximum),
}),
});
}
blank_count = 0;
}
}
if blank_count > self.maximum {
warnings.push(LintWarning {
message: format!(
"Multiple consecutive blank lines at end of file ({} > {})",
blank_count, self.maximum
),
line: blank_start + 1,
column: 1,
fix: Some(Fix {
line: blank_start + 1,
column: 1,
replacement: "\n".repeat(self.maximum),
}),
});
}
Ok(warnings)
}
fn fix(&self, content: &str) -> Result<String, LintError> {
let mut result = Vec::new();
let mut blank_count = 0;
let lines: Vec<&str> = content.lines().collect();
let mut in_code_block = false;
let mut in_front_matter = false;
let mut code_block_blanks = Vec::new();
for (_i, &line) in lines.iter().enumerate() {
if line.trim_start().starts_with("```") || line.trim_start().starts_with("~~~") {
if !in_code_block {
let allowed_blanks = blank_count.min(self.maximum);
if allowed_blanks > 0 {
result.extend(vec![""; allowed_blanks]);
}
blank_count = 0;
} else {
result.extend(code_block_blanks.drain(..));
}
in_code_block = !in_code_block;
result.push(line);
continue;
}
if line.trim() == "---" {
in_front_matter = !in_front_matter;
if blank_count > 0 {
result.extend(vec![""; blank_count]);
blank_count = 0;
}
result.push(line);
continue;
}
if in_code_block {
if line.trim().is_empty() {
code_block_blanks.push(line);
} else {
result.extend(code_block_blanks.drain(..));
result.push(line);
}
} else if in_front_matter {
if blank_count > 0 {
result.extend(vec![""; blank_count]);
blank_count = 0;
}
result.push(line);
} else if line.trim().is_empty() {
blank_count += 1;
} else {
let allowed_blanks = blank_count.min(self.maximum);
if allowed_blanks > 0 {
result.extend(vec![""; allowed_blanks]);
}
blank_count = 0;
result.push(line);
}
}
if !in_code_block {
let allowed_blanks = blank_count.min(self.maximum);
if allowed_blanks > 0 {
result.extend(vec![""; allowed_blanks]);
}
}
let mut output = result.join("\n");
if content.ends_with('\n') {
output.push('\n');
}
Ok(output)
}
}