Skip to main content

mdlint/lint/rules/
md045.rs

1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use pulldown_cmark::{Event, Tag, TagEnd};
5use serde_json::Value;
6
7pub struct MD045;
8
9impl Rule for MD045 {
10    fn name(&self) -> &str {
11        "MD045"
12    }
13
14    fn description(&self) -> &str {
15        "Images should have alternate text (alt text)"
16    }
17
18    fn tags(&self) -> &[&str] {
19        &["accessibility", "images"]
20    }
21
22    fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
23        let mut violations = Vec::new();
24        let mut in_image = false;
25        let mut image_start_line = 0;
26        let mut alt_text = String::new();
27
28        for (event, range) in parser.parse_with_offsets() {
29            match event {
30                Event::Start(Tag::Image { .. }) => {
31                    in_image = true;
32                    image_start_line = parser.offset_to_line(range.start);
33                    alt_text.clear();
34                }
35                Event::Text(text) if in_image => {
36                    alt_text.push_str(&text);
37                }
38                Event::End(TagEnd::Image) if in_image => {
39                    // Only report if alt_text is completely empty (not just whitespace)
40                    if alt_text.is_empty() {
41                        violations.push(Violation {
42                            line: image_start_line,
43                            column: Some(1),
44                            rule: self.name().to_string(),
45                            message: "Images should have alternate text (alt text)".to_string(),
46                            fix: None,
47                        });
48                    }
49                    in_image = false;
50                }
51                _ => {}
52            }
53        }
54
55        violations
56    }
57
58    fn fixable(&self) -> bool {
59        false
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn test_image_with_alt() {
69        let content = "![Alt text](image.png)";
70        let parser = MarkdownParser::new(content);
71        let rule = MD045;
72        let violations = rule.check(&parser, None);
73
74        assert_eq!(violations.len(), 0);
75    }
76
77    #[test]
78    fn test_image_without_alt() {
79        let content = "![](image.png)";
80        let parser = MarkdownParser::new(content);
81        let rule = MD045;
82        let violations = rule.check(&parser, None);
83
84        assert_eq!(violations.len(), 1);
85    }
86
87    #[test]
88    fn test_image_with_whitespace_alt() {
89        let content = "![  ](image.png)";
90        let parser = MarkdownParser::new(content);
91        let rule = MD045;
92        let violations = rule.check(&parser, None);
93
94        assert_eq!(violations.len(), 0); // Whitespace-only is considered valid
95    }
96
97    #[test]
98    fn test_multiple_images() {
99        let content = "![Good](img1.png) and ![](img2.png)";
100        let parser = MarkdownParser::new(content);
101        let rule = MD045;
102        let violations = rule.check(&parser, None);
103
104        assert_eq!(violations.len(), 1); // Only second image lacks alt
105    }
106}