use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule};
use regex::Regex;
use lazy_static::lazy_static;
lazy_static! {
static ref HR_DASH: Regex = Regex::new(r"^\-{3,}\s*$").unwrap();
static ref HR_ASTERISK: Regex = Regex::new(r"^\*{3,}\s*$").unwrap();
static ref HR_UNDERSCORE: Regex = Regex::new(r"^_{3,}\s*$").unwrap();
static ref HR_SPACED_DASH: Regex = Regex::new(r"^(\-\s+){2,}\-\s*$").unwrap();
static ref HR_SPACED_ASTERISK: Regex = Regex::new(r"^(\*\s+){2,}\*\s*$").unwrap();
static ref HR_SPACED_UNDERSCORE: Regex = Regex::new(r"^(_\s+){2,}_\s*$").unwrap();
}
#[derive(Debug)]
pub struct MD035HRStyle {
style: String,
}
impl Default for MD035HRStyle {
fn default() -> Self {
Self {
style: "---".to_string(),
}
}
}
impl MD035HRStyle {
pub fn new(style: String) -> Self {
Self { style }
}
fn is_horizontal_rule(line: &str) -> bool {
let line = line.trim();
HR_DASH.is_match(line) ||
HR_ASTERISK.is_match(line) ||
HR_UNDERSCORE.is_match(line) ||
HR_SPACED_DASH.is_match(line) ||
HR_SPACED_ASTERISK.is_match(line) ||
HR_SPACED_UNDERSCORE.is_match(line)
}
fn is_potential_setext_heading(lines: &[&str], i: usize) -> bool {
if i == 0 {
return false; }
let line = lines[i].trim();
let prev_line = lines[i - 1].trim();
let is_dash_line = !line.is_empty() && line.chars().all(|c| c == '-');
let is_equals_line = !line.is_empty() && line.chars().all(|c| c == '=');
let prev_line_has_content = !prev_line.is_empty() && !Self::is_horizontal_rule(prev_line);
(is_dash_line || is_equals_line) && prev_line_has_content
}
}
impl Rule for MD035HRStyle {
fn name(&self) -> &'static str {
"MD035"
}
fn description(&self) -> &'static str {
"Horizontal rule style"
}
fn check(&self, content: &str) -> LintResult {
let mut warnings = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let expected_style = if self.style.is_empty() {
let mut first_style = "---".to_string(); for (i, line) in lines.iter().enumerate() {
if Self::is_horizontal_rule(line) && !Self::is_potential_setext_heading(&lines, i) {
first_style = line.trim().to_string();
break;
}
}
first_style
} else {
self.style.clone()
};
for (i, line) in lines.iter().enumerate() {
if Self::is_potential_setext_heading(&lines, i) {
continue;
}
if Self::is_horizontal_rule(line) {
let has_indentation = line.len() > line.trim_start().len();
let style_mismatch = line.trim() != expected_style;
if style_mismatch || has_indentation {
warnings.push(LintWarning {
line: i + 1,
column: 1,
message: if has_indentation {
"Horizontal rule should not be indented".to_string()
} else {
format!("Horizontal rule style should be \"{}\"", expected_style)
},
fix: Some(Fix {
line: i + 1,
column: 1,
replacement: expected_style.clone(),
}),
});
}
}
}
Ok(warnings)
}
fn fix(&self, content: &str) -> Result<String, LintError> {
let mut result = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let expected_style = if self.style.is_empty() {
let mut first_style = "---".to_string(); for (i, line) in lines.iter().enumerate() {
if Self::is_horizontal_rule(line) && !Self::is_potential_setext_heading(&lines, i) {
first_style = line.trim().to_string();
break;
}
}
first_style
} else {
self.style.clone()
};
for (i, line) in lines.iter().enumerate() {
if Self::is_potential_setext_heading(&lines, i) {
result.push(line.to_string());
continue;
}
if Self::is_horizontal_rule(line) {
result.push(expected_style.clone());
} else {
result.push(line.to_string());
}
}
Ok(result.join("\n"))
}
}