use crate::lint::rule::Rule;
use crate::markdown::MarkdownParser;
use crate::types::Violation;
use pulldown_cmark::Event;
use serde_json::Value;
pub struct MD037;
impl MD037 {
fn check_pattern(
text: &str,
base_offset: usize,
marker: &str,
parser: &MarkdownParser,
violations: &mut Vec<Violation>,
) {
let marker_len = marker.len();
let open_pattern = format!("{} ", marker);
let close_pattern = format!(" {}", marker);
let mut offset = 0;
while let Some(pos) = text[offset..].find(&open_pattern) {
let start_pos = offset + pos;
let search_start = start_pos + marker_len + 1;
if let Some(end_offset) = text[search_start..].find(&close_pattern) {
let end_pos = search_start + end_offset;
let between = &text[search_start..end_pos];
if !between.contains(marker) {
let abs_start = base_offset + start_pos;
let (line, col) = parser.offset_to_position(abs_start);
violations.push(Violation {
line,
column: Some(col),
rule: "MD037".to_string(),
message: "Spaces inside emphasis markers".to_string(),
fix: None,
});
let abs_end = base_offset + end_pos;
let (end_line, end_col) = parser.offset_to_position(abs_end);
violations.push(Violation {
line: end_line,
column: Some(end_col),
rule: "MD037".to_string(),
message: "Spaces inside emphasis markers".to_string(),
fix: None,
});
}
}
offset = start_pos + 1;
}
}
fn check_single_marker_pattern(
text: &str,
base_offset: usize,
marker: &str,
parser: &MarkdownParser,
violations: &mut Vec<Violation>,
) {
let open_pattern = format!("{} ", marker);
let close_pattern = format!(" {}", marker);
let double_marker = marker.repeat(2);
let mut offset = 0;
while let Some(pos) = text[offset..].find(&open_pattern) {
let start_pos = offset + pos;
let before = if start_pos > 0 {
&text[start_pos.saturating_sub(1)..start_pos]
} else {
""
};
let after_marker = start_pos + marker.len();
let after = if after_marker + 1 < text.len() {
&text[after_marker + 1..after_marker + 2]
} else {
""
};
if before == marker || after == marker {
offset = start_pos + 1;
continue;
}
let search_start = start_pos + marker.len() + 1;
if let Some(end_offset) = text[search_start..].find(&close_pattern) {
let end_pos = search_start + end_offset;
let between = &text[search_start..end_pos];
if !between.contains(marker) && !between.contains(&double_marker) {
let before_close = end_pos.saturating_sub(1);
let after_close_marker = end_pos + marker.len() + 1;
let before_close_char = if before_close < search_start {
""
} else {
&text[before_close..before_close + 1]
};
let after_close_char = if after_close_marker < text.len() {
&text[after_close_marker..after_close_marker + 1]
} else {
""
};
if before_close_char == marker || after_close_char == marker {
offset = start_pos + 1;
continue;
}
let abs_start = base_offset + start_pos;
let (line, col) = parser.offset_to_position(abs_start);
violations.push(Violation {
line,
column: Some(col),
rule: "MD037".to_string(),
message: "Spaces inside emphasis markers".to_string(),
fix: None,
});
let abs_end = base_offset + end_pos;
let (end_line, end_col) = parser.offset_to_position(abs_end);
violations.push(Violation {
line: end_line,
column: Some(end_col),
rule: "MD037".to_string(),
message: "Spaces inside emphasis markers".to_string(),
fix: None,
});
}
}
offset = start_pos + 1;
}
}
}
impl Rule for MD037 {
fn name(&self) -> &str {
"MD037"
}
fn description(&self) -> &str {
"Spaces inside emphasis markers"
}
fn tags(&self) -> &[&str] {
&["whitespace", "emphasis"]
}
fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
let mut violations = Vec::new();
let events: Vec<_> = parser.parse_with_offsets().collect();
let code_ranges = parser.get_code_ranges();
for (event, range) in events.iter() {
if let Event::Text(text) = event {
let text_str = text.as_ref();
let offset = range.start;
let in_code = code_ranges.iter().any(|r| r.contains(&offset));
if in_code {
continue;
}
Self::check_pattern(text_str, offset, "**", parser, &mut violations);
Self::check_pattern(text_str, offset, "__", parser, &mut violations);
Self::check_single_marker_pattern(text_str, offset, "*", parser, &mut violations);
Self::check_single_marker_pattern(text_str, offset, "_", parser, &mut violations);
}
}
violations
}
fn fixable(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_correct_emphasis() {
let content = "This is **bold** and *italic* text.";
let parser = MarkdownParser::new(content);
let rule = MD037;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
#[test]
fn test_spaces_in_strong() {
let content = "This is ** bold ** text.";
let parser = MarkdownParser::new(content);
let rule = MD037;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 2); }
#[test]
fn test_spaces_in_emphasis() {
let content = "This is * italic * text.";
let parser = MarkdownParser::new(content);
let rule = MD037;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 2); }
#[test]
fn test_underscores() {
let content = "This is __ bold __ text.";
let parser = MarkdownParser::new(content);
let rule = MD037;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 2); }
#[test]
fn test_code_block_with_underscores() {
let content = "Normal text\n\n```sql\nCREATE POLICY territory_contact_access ON contacts\n FOR SELECT\n USING (\n territory_id IN (\n SELECT territory_id\n FROM user_territory_assignments\n WHERE user_id = current_setting('app.current_user_id')::uuid\n AND (valid_to IS NULL OR valid_to > NOW())\n )\n );\n```\n\nMore text";
let parser = MarkdownParser::new(content);
let rule = MD037;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
#[test]
fn test_inline_code_with_underscores() {
let content = "Use the `user_id` variable for * 2 * 3 multiplication.";
let parser = MarkdownParser::new(content);
let rule = MD037;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 2); }
#[test]
fn test_typescript_multiplication() {
let content = "```typescript\nconst result = value_a * value_b * value_c;\n```";
let parser = MarkdownParser::new(content);
let rule = MD037;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
#[test]
fn test_adjacent_emphasis_blocks() {
let content = "**CASL** or **Permify**: Attribute-based access control";
let parser = MarkdownParser::new(content);
let rule = MD037;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
#[test]
fn test_multiple_bold_words() {
let content = "Use **bold** and **more bold** text.";
let parser = MarkdownParser::new(content);
let rule = MD037;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
}