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