use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule};
#[derive(Debug)]
pub struct MD058BlanksAroundTables;
impl MD058BlanksAroundTables {
fn is_in_code_block(&self, lines: &[&str], line_index: usize) -> bool {
let mut in_code_block = false;
let mut code_fence = "";
for (i, line) in lines.iter().enumerate() {
if i > line_index {
break;
}
let trimmed = line.trim();
if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
if !in_code_block {
in_code_block = true;
code_fence = if trimmed.starts_with("```") { "```" } else { "~~~" };
} else if trimmed.starts_with(code_fence) {
in_code_block = false;
}
}
if i == line_index && in_code_block {
return true;
}
}
false
}
fn is_table_row(&self, line: &str) -> bool {
let trimmed = line.trim();
trimmed.contains('|')
}
fn is_delimiter_row(&self, line: &str) -> bool {
let trimmed = line.trim();
trimmed.contains('|') &&
trimmed.chars().all(|c| c == '|' || c == '-' || c == ':' || c.is_whitespace())
}
fn is_blank_line(&self, line: &str) -> bool {
line.trim().is_empty()
}
fn identify_tables(&self, lines: &[&str]) -> Vec<(usize, usize)> {
let mut tables = Vec::new();
let mut current_table_start: Option<usize> = None;
let mut found_delimiter = false;
for (i, line) in lines.iter().enumerate() {
if self.is_in_code_block(lines, i) {
continue;
}
let is_table_row = self.is_table_row(line);
let is_delimiter = self.is_delimiter_row(line);
if is_delimiter {
found_delimiter = true;
}
if is_table_row {
if current_table_start.is_none() {
current_table_start = Some(i);
}
} else if current_table_start.is_some() && !is_table_row {
if let Some(start) = current_table_start {
if found_delimiter {
tables.push((start, i - 1));
}
}
current_table_start = None;
found_delimiter = false;
}
}
if let Some(start) = current_table_start {
if found_delimiter {
tables.push((start, lines.len() - 1));
}
}
tables
}
}
impl Rule for MD058BlanksAroundTables {
fn name(&self) -> &'static str {
"MD058"
}
fn description(&self) -> &'static str {
"Tables should be surrounded by blank lines"
}
fn check(&self, content: &str) -> LintResult {
let mut warnings = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let tables = self.identify_tables(&lines);
for (table_start, table_end) in tables {
if table_start > 0 && !self.is_blank_line(lines[table_start - 1]) {
warnings.push(LintWarning {
line: table_start + 1,
column: 1,
message: "Missing blank line before table".to_string(),
fix: Some(Fix {
line: table_start + 1,
column: 1,
replacement: format!("\n{}", lines[table_start]),
}),
});
}
if table_end < lines.len() - 1 && !self.is_blank_line(lines[table_end + 1]) {
warnings.push(LintWarning {
line: table_end + 1,
column: lines[table_end].len() + 1,
message: "Missing blank line after table".to_string(),
fix: Some(Fix {
line: table_end + 1,
column: lines[table_end].len() + 1,
replacement: format!("{}\n", lines[table_end]),
}),
});
}
}
Ok(warnings)
}
fn fix(&self, content: &str) -> Result<String, LintError> {
let mut warnings = self.check(content)?;
if warnings.is_empty() {
return Ok(content.to_string());
}
let lines: Vec<&str> = content.lines().collect();
let mut result = Vec::new();
let mut i = 0;
while i < lines.len() {
let warning_before = warnings.iter().position(|w| {
w.line == i + 1 && w.message == "Missing blank line before table"
});
if let Some(idx) = warning_before {
result.push("".to_string());
warnings.remove(idx);
}
result.push(lines[i].to_string());
let warning_after = warnings.iter().position(|w| {
w.line == i + 1 && w.message == "Missing blank line after table"
});
if let Some(idx) = warning_after {
result.push("".to_string());
warnings.remove(idx);
}
i += 1;
}
Ok(result.join("\n"))
}
}