mdbook_lint_core/rules/standard/
md045.rs1use crate::error::Result;
6use crate::rule::{AstRule, RuleCategory, RuleMetadata};
7use crate::{
8 Document,
9 violation::{Severity, Violation},
10};
11use comrak::nodes::{AstNode, NodeValue};
12
13pub struct MD045;
15
16impl MD045 {
17 fn is_empty_alt_text<'a>(&self, node: &'a AstNode<'a>) -> bool {
19 let text_content = Self::extract_text_content(node);
21 text_content.trim().is_empty()
22 }
23
24 fn extract_text_content<'a>(node: &'a AstNode<'a>) -> String {
26 let mut content = String::new();
27
28 match &node.data.borrow().value {
29 NodeValue::Text(text) => {
30 content.push_str(text);
31 }
32 NodeValue::Code(code) => {
33 content.push_str(&code.literal);
34 }
35 _ => {}
36 }
37
38 for child in node.children() {
40 content.push_str(&Self::extract_text_content(child));
41 }
42
43 content
44 }
45
46 fn get_position<'a>(&self, node: &'a AstNode<'a>) -> (usize, usize) {
48 let data = node.data.borrow();
49 let pos = data.sourcepos;
50 (pos.start.line, pos.start.column)
51 }
52
53 fn check_node<'a>(&self, node: &'a AstNode<'a>, violations: &mut Vec<Violation>) {
55 if let NodeValue::Image(_) = &node.data.borrow().value
56 && self.is_empty_alt_text(node)
57 {
58 let (line, column) = self.get_position(node);
59 violations.push(self.create_violation(
60 "Images should have alternate text".to_string(),
61 line,
62 column,
63 Severity::Warning,
64 ));
65 }
66
67 for child in node.children() {
69 self.check_node(child, violations);
70 }
71 }
72}
73
74impl AstRule for MD045 {
75 fn id(&self) -> &'static str {
76 "MD045"
77 }
78
79 fn name(&self) -> &'static str {
80 "no-alt-text"
81 }
82
83 fn description(&self) -> &'static str {
84 "Images should have alternate text"
85 }
86
87 fn metadata(&self) -> RuleMetadata {
88 RuleMetadata::stable(RuleCategory::Content).introduced_in("mdbook-lint v0.1.0")
89 }
90
91 fn check_ast<'a>(&self, _document: &Document, ast: &'a AstNode<'a>) -> Result<Vec<Violation>> {
92 let mut violations = Vec::new();
93 self.check_node(ast, &mut violations);
94 Ok(violations)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::rule::Rule;
102 use std::path::PathBuf;
103
104 fn create_test_document(content: &str) -> Document {
105 Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
106 }
107
108 #[test]
109 fn test_md045_images_with_alt_text_valid() {
110 let content = r#"Here is an image with alt text: .
111
112Another  here.
113
114And a reference image: ![alt text][ref]
115
116[ref]: image3.gif
117"#;
118
119 let document = create_test_document(content);
120 let rule = MD045;
121 let violations = rule.check(&document).unwrap();
122 assert_eq!(violations.len(), 0);
123 }
124
125 #[test]
126 fn test_md045_images_without_alt_text_violation() {
127 let content = r#"Here is an image without alt text: .
128
129Another  here.
130"#;
131
132 let document = create_test_document(content);
133 let rule = MD045;
134 let violations = rule.check(&document).unwrap();
135 assert_eq!(violations.len(), 2);
136 assert_eq!(violations[0].rule_id, "MD045");
137 assert!(
138 violations[0]
139 .message
140 .contains("Images should have alternate text")
141 );
142 assert_eq!(violations[0].line, 1);
143 assert_eq!(violations[1].line, 3);
144 }
145
146 #[test]
147 fn test_md045_images_with_whitespace_only_alt_text() {
148 let content = r#"Image with spaces: .
149
150Image with tabs: .
151
152Image with mixed whitespace: .
153"#;
154
155 let document = create_test_document(content);
156 let rule = MD045;
157 let violations = rule.check(&document).unwrap();
158 assert_eq!(violations.len(), 3);
159 assert_eq!(violations[0].line, 1);
160 assert_eq!(violations[1].line, 3);
161 assert_eq!(violations[2].line, 5);
162 }
163
164 #[test]
165 fn test_md045_images_with_code_alt_text_valid() {
166 let content = r#"Image with code alt text: .
167
168Another with inline code: .
169"#;
170
171 let document = create_test_document(content);
172 let rule = MD045;
173 let violations = rule.check(&document).unwrap();
174 assert_eq!(violations.len(), 0);
175 }
176
177 #[test]
178 fn test_md045_images_with_emphasis_alt_text_valid() {
179 let content = r#"Image with emphasis: .
180
181Image with strong: .
182
183Image with mixed: .
184"#;
185
186 let document = create_test_document(content);
187 let rule = MD045;
188 let violations = rule.check(&document).unwrap();
189 assert_eq!(violations.len(), 0);
190 }
191
192 #[test]
193 fn test_md045_reference_images() {
194 let content = r#"Good reference image: ![Good alt text][good].
195
196Bad reference image: ![][bad].
197
198Another bad one: ![ ][also-bad].
199
200[good]: image1.png
201[bad]: image2.png
202[also-bad]: image3.png
203"#;
204
205 let document = create_test_document(content);
206 let rule = MD045;
207 let violations = rule.check(&document).unwrap();
208 assert_eq!(violations.len(), 2);
209 assert_eq!(violations[0].line, 3);
210 assert_eq!(violations[1].line, 5);
211 }
212
213 #[test]
214 fn test_md045_links_ignored() {
215 let content = r#"This is a [link without text]() which should not be flagged.
216
217This is a [](http://example.com) empty link, also not flagged by this rule.
218
219But this  empty image should be flagged.
220"#;
221
222 let document = create_test_document(content);
223 let rule = MD045;
224 let violations = rule.check(&document).unwrap();
225 assert_eq!(violations.len(), 1);
226 assert_eq!(violations[0].line, 5);
227 }
228
229 #[test]
230 fn test_md045_mixed_images_and_links() {
231 let content = r#"Good image:  and good [link](http://example.com).
232
233Bad image:  and empty [](http://example.com) link.
234
235Another good image:  here.
236"#;
237
238 let document = create_test_document(content);
239 let rule = MD045;
240 let violations = rule.check(&document).unwrap();
241 assert_eq!(violations.len(), 1);
242 assert_eq!(violations[0].line, 3);
243 }
244
245 #[test]
246 fn test_md045_nested_formatting_in_alt_text() {
247 let content = r#"Complex alt text: .
248
249Simple alt text: .
250
251Empty alt text: .
252"#;
253
254 let document = create_test_document(content);
255 let rule = MD045;
256 let violations = rule.check(&document).unwrap();
257 assert_eq!(violations.len(), 1);
258 assert_eq!(violations[0].line, 5);
259 }
260
261 #[test]
262 fn test_md045_inline_images() {
263 let content = r#"Text with inline  image.
264
265Text with inline  empty image.
266
267More text with  alt text.
268"#;
269
270 let document = create_test_document(content);
271 let rule = MD045;
272 let violations = rule.check(&document).unwrap();
273 assert_eq!(violations.len(), 1);
274 assert_eq!(violations[0].line, 3);
275 }
276
277 #[test]
278 fn test_md045_multiple_images_per_line() {
279 let content = r#"Multiple images:  and  and .
280
281All good:  and .
282
283All bad:  and .
284"#;
285
286 let document = create_test_document(content);
287 let rule = MD045;
288 let violations = rule.check(&document).unwrap();
289 assert_eq!(violations.len(), 3);
290 assert_eq!(violations[0].line, 1); assert_eq!(violations[1].line, 5); assert_eq!(violations[2].line, 5); }
294}