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