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