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