use crate::lint::rule::Rule;
use crate::markdown::MarkdownParser;
use crate::types::Violation;
use pulldown_cmark::{Event, Tag};
use regex::Regex;
use serde_json::Value;
pub struct MD011;
impl Rule for MD011 {
fn name(&self) -> &str {
"MD011"
}
fn description(&self) -> &str {
"Reversed link syntax"
}
fn tags(&self) -> &[&str] {
&["links"]
}
fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
let mut violations = Vec::new();
let mut code_block_lines = std::collections::HashSet::new();
let mut in_code_block = false;
for (event, range) in parser.parse_with_offsets() {
match event {
Event::Start(Tag::CodeBlock(_)) => {
in_code_block = true;
}
Event::End(Tag::CodeBlock(_)) => {
in_code_block = false;
}
Event::Text(_) if in_code_block => {
let start_line = parser.offset_to_line(range.start);
let end_line = parser.offset_to_line(range.end.saturating_sub(1));
for line in start_line..=end_line {
code_block_lines.insert(line);
}
}
_ => {}
}
}
let re = Regex::new(r"\([^)]+\)\[[^\]]+\]").unwrap();
for (line_num, line) in parser.lines().iter().enumerate() {
let line_number = line_num + 1;
if code_block_lines.contains(&line_number) {
continue;
}
for m in re.find_iter(line) {
violations.push(Violation {
line: line_number,
column: Some(m.start() + 1),
rule: self.name().to_string(),
message: "Reversed link syntax (found '(text)[url]', should be '[text](url)')"
.to_string(),
fix: None,
});
}
}
violations
}
fn fixable(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_correct_link_syntax() {
let content = "This is [a link](http://example.com) and [another](url).";
let parser = MarkdownParser::new(content);
let rule = MD011;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
#[test]
fn test_reversed_link_syntax() {
let content = "This is (a link)[http://example.com] which is wrong.";
let parser = MarkdownParser::new(content);
let rule = MD011;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].line, 1);
}
#[test]
fn test_multiple_reversed_links() {
let content = "First (link)[url1] and second (link)[url2].";
let parser = MarkdownParser::new(content);
let rule = MD011;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 2);
}
#[test]
fn test_mixed_correct_and_reversed() {
let content = "Correct [link](url) and (reversed)[url].";
let parser = MarkdownParser::new(content);
let rule = MD011;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].line, 1);
}
#[test]
fn test_no_false_positives() {
let content = "Some (parentheses) and [brackets] but not links.";
let parser = MarkdownParser::new(content);
let rule = MD011;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
#[test]
fn test_code_block_not_flagged() {
let content = r#"# Code Example
```python
result = function(param)[index]
data = array(0)[key]
```
This (is)[wrong] though.
"#;
let parser = MarkdownParser::new(content);
let rule = MD011;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].line, 8);
}
}