rumdl_lib/rules/
md045_no_alt_text.rs1use crate::rule::{FixCapability, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2
3pub mod md045_config;
4use md045_config::MD045Config;
5
6#[derive(Clone)]
15pub struct MD045NoAltText {
16 config: MD045Config,
17}
18
19impl Default for MD045NoAltText {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl MD045NoAltText {
26 pub fn new() -> Self {
27 Self {
28 config: MD045Config::default(),
29 }
30 }
31
32 pub fn from_config_struct(config: MD045Config) -> Self {
33 Self { config }
34 }
35}
36
37impl Rule for MD045NoAltText {
38 fn name(&self) -> &'static str {
39 "MD045"
40 }
41
42 fn description(&self) -> &'static str {
43 "Images should have alternate text (alt text)"
44 }
45
46 fn category(&self) -> RuleCategory {
47 RuleCategory::Image
48 }
49
50 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
51 !ctx.likely_has_links_or_images()
53 }
54
55 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
56 let mut warnings = Vec::new();
57
58 for image in &ctx.images {
59 if image.alt_text.trim().is_empty() {
60 warnings.push(LintWarning {
61 rule_name: Some(self.name().to_string()),
62 line: image.line,
63 column: image.start_col + 1,
64 end_line: image.line,
65 end_column: image.end_col + 1,
66 message: "Image missing alt text (add description for accessibility: )"
67 .to_string(),
68 severity: Severity::Error,
69 fix: None,
70 });
71 }
72 }
73
74 Ok(warnings)
75 }
76
77 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
78 Ok(ctx.content.to_string())
79 }
80
81 fn fix_capability(&self) -> FixCapability {
82 FixCapability::Unfixable
83 }
84
85 fn as_any(&self) -> &dyn std::any::Any {
86 self
87 }
88
89 fn default_config_section(&self) -> Option<(String, toml::Value)> {
90 let json_value = serde_json::to_value(&self.config).ok()?;
91 Some((
92 self.name().to_string(),
93 crate::rule_config_serde::json_to_toml_value(&json_value)?,
94 ))
95 }
96
97 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
98 where
99 Self: Sized,
100 {
101 let rule_config = crate::rule_config_serde::load_rule_config::<MD045Config>(config);
102 Box::new(Self::from_config_struct(rule_config))
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::lint_context::LintContext;
110
111 #[test]
112 fn test_image_with_alt_text() {
113 let rule = MD045NoAltText::new();
114 let content = "";
115 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
116 let result = rule.check(&ctx).unwrap();
117
118 assert_eq!(result.len(), 0);
119 }
120
121 #[test]
122 fn test_image_without_alt_text() {
123 let rule = MD045NoAltText::new();
124 let content = "";
125 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
126 let result = rule.check(&ctx).unwrap();
127
128 assert_eq!(result.len(), 1);
129 assert_eq!(result[0].line, 1);
130 assert!(result[0].message.contains("Image missing alt text"));
131 }
132
133 #[test]
134 fn test_no_fix_offered() {
135 let rule = MD045NoAltText::new();
136 let content = "";
137 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
138 let result = rule.check(&ctx).unwrap();
139
140 assert_eq!(result.len(), 1);
141 assert!(
142 result[0].fix.is_none(),
143 "MD045 should not offer auto-fix (alt text requires human judgment)"
144 );
145 }
146
147 #[test]
148 fn test_image_with_only_whitespace_alt_text() {
149 let rule = MD045NoAltText::new();
150 let content = "";
151 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
152 let result = rule.check(&ctx).unwrap();
153
154 assert_eq!(result.len(), 1);
155 assert_eq!(result[0].line, 1);
156 assert!(result[0].fix.is_none());
157 }
158
159 #[test]
160 fn test_multiple_images() {
161 let rule = MD045NoAltText::new();
162 let content = "\n\n\n";
163 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
164 let result = rule.check(&ctx).unwrap();
165
166 assert_eq!(result.len(), 2);
167 assert_eq!(result[0].line, 2);
168 assert_eq!(result[1].line, 4);
169 }
170
171 #[test]
172 fn test_reference_style_image() {
173 let rule = MD045NoAltText::new();
174 let content = "![][sunset]\n\n[sunset]: sunset.jpg";
175 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
176 let result = rule.check(&ctx).unwrap();
177
178 assert_eq!(result.len(), 1);
179 assert_eq!(result[0].line, 1);
180 }
181
182 #[test]
183 fn test_reference_style_with_alt_text() {
184 let rule = MD045NoAltText::new();
185 let content = "![Beautiful sunset][sunset]\n\n[sunset]: sunset.jpg";
186 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
187 let result = rule.check(&ctx).unwrap();
188
189 assert_eq!(result.len(), 0);
190 }
191
192 #[test]
193 fn test_image_in_code_block() {
194 let rule = MD045NoAltText::new();
195 let content = "```\n\n```";
196 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
197 let result = rule.check(&ctx).unwrap();
198
199 assert_eq!(result.len(), 0);
200 }
201
202 #[test]
203 fn test_image_in_inline_code() {
204 let rule = MD045NoAltText::new();
205 let content = "Use `` syntax";
206 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
207 let result = rule.check(&ctx).unwrap();
208
209 assert_eq!(result.len(), 0);
210 }
211
212 #[test]
213 fn test_complex_urls() {
214 let rule = MD045NoAltText::new();
215 let content = "";
216 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
217 let result = rule.check(&ctx).unwrap();
218
219 assert_eq!(result.len(), 1);
220 }
221
222 #[test]
223 fn test_image_with_title() {
224 let rule = MD045NoAltText::new();
225 let content = "";
226 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
227 let result = rule.check(&ctx).unwrap();
228
229 assert_eq!(result.len(), 1);
230 assert!(result[0].message.contains("Image missing alt text"));
231 }
232
233 #[test]
234 fn test_column_positions() {
235 let rule = MD045NoAltText::new();
236 let content = "Text before  text after";
237 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
238 let result = rule.check(&ctx).unwrap();
239
240 assert_eq!(result.len(), 1);
241 assert_eq!(result[0].line, 1);
242 assert_eq!(result[0].column, 13);
243 }
244
245 #[test]
246 fn test_multiline_content() {
247 let rule = MD045NoAltText::new();
248 let content = "Line 1\nLine 2 with \nLine 3";
249 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
250 let result = rule.check(&ctx).unwrap();
251
252 assert_eq!(result.len(), 1);
253 assert_eq!(result[0].line, 2);
254 }
255}