mdbook_lint_core/rules/standard/
md042.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 MD042;
15
16impl MD042 {
17 fn is_empty_link<'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 match &node.data.borrow().value {
56 NodeValue::Link(_) => {
57 if self.is_empty_link(node) {
58 let (line, column) = self.get_position(node);
59 violations.push(self.create_violation(
60 "Found empty link".to_string(),
61 line,
62 column,
63 Severity::Warning,
64 ));
65 }
66 }
67 NodeValue::Image(_) => {
68 if self.is_empty_link(node) {
70 let (line, column) = self.get_position(node);
71 violations.push(self.create_violation(
72 "Found image with empty alt text".to_string(),
73 line,
74 column,
75 Severity::Warning,
76 ));
77 }
78 }
79 _ => {}
80 }
81
82 for child in node.children() {
84 self.check_node(child, violations);
85 }
86 }
87}
88
89impl AstRule for MD042 {
90 fn id(&self) -> &'static str {
91 "MD042"
92 }
93
94 fn name(&self) -> &'static str {
95 "no-empty-links"
96 }
97
98 fn description(&self) -> &'static str {
99 "No empty links"
100 }
101
102 fn metadata(&self) -> RuleMetadata {
103 RuleMetadata::stable(RuleCategory::Content).introduced_in("mdbook-lint v0.1.0")
104 }
105
106 fn check_ast<'a>(&self, _document: &Document, ast: &'a AstNode<'a>) -> Result<Vec<Violation>> {
107 let mut violations = Vec::new();
108 self.check_node(ast, &mut violations);
109 Ok(violations)
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::rule::Rule;
117 use std::path::PathBuf;
118
119 fn create_test_document(content: &str) -> Document {
120 Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
121 }
122
123 #[test]
124 fn test_md042_normal_links_valid() {
125 let content = r#"Here is a [normal link](http://example.com).
126
127Another [link with text](http://example.com) works fine.
128
129Reference link [with text][ref] is also okay.
130
131[ref]: http://example.com
132"#;
133
134 let document = create_test_document(content);
135 let rule = MD042;
136 let violations = rule.check(&document).unwrap();
137 assert_eq!(violations.len(), 0);
138 }
139
140 #[test]
141 fn test_md042_empty_inline_link() {
142 let content = r#"Here is an [](http://example.com) empty link.
143
144This is normal text with a problem [](http://bad.com) link.
145"#;
146
147 let document = create_test_document(content);
148 let rule = MD042;
149 let violations = rule.check(&document).unwrap();
150 assert_eq!(violations.len(), 2);
151 assert_eq!(violations[0].rule_id, "MD042");
152 assert!(violations[0].message.contains("Found empty link"));
153 assert_eq!(violations[0].line, 1);
154 assert_eq!(violations[1].line, 3);
155 }
156
157 #[test]
158 fn test_md042_empty_reference_link() {
159 let content = r#"Here is an [][ref] empty reference link.
160
161[ref]: http://example.com
162"#;
163
164 let document = create_test_document(content);
165 let rule = MD042;
166 let violations = rule.check(&document).unwrap();
167 assert_eq!(violations.len(), 1);
168 assert_eq!(violations[0].line, 1);
169 }
170
171 #[test]
172 fn test_md042_whitespace_only_link() {
173 let content = r#"Here is a [ ](http://example.com) whitespace-only link.
174
175Another [ ](http://example.com) tab-only link.
176"#;
177
178 let document = create_test_document(content);
179 let rule = MD042;
180 let violations = rule.check(&document).unwrap();
181 assert_eq!(violations.len(), 2);
182 assert_eq!(violations[0].line, 1);
183 assert_eq!(violations[1].line, 3);
184 }
185
186 #[test]
187 fn test_md042_link_with_code_valid() {
188 let content = r#"Here is a [`code`](http://example.com) link with code.
189
190Another [normal text](http://example.com) link.
191"#;
192
193 let document = create_test_document(content);
194 let rule = MD042;
195 let violations = rule.check(&document).unwrap();
196 assert_eq!(violations.len(), 0);
197 }
198
199 #[test]
200 fn test_md042_link_with_emphasis_valid() {
201 let content = r#"Here is a [*emphasized*](http://example.com) link.
202
203Another [**strong**](http://example.com) link.
204
205And [_underlined_](http://example.com) text.
206"#;
207
208 let document = create_test_document(content);
209 let rule = MD042;
210 let violations = rule.check(&document).unwrap();
211 assert_eq!(violations.len(), 0);
212 }
213
214 #[test]
215 fn test_md042_empty_image_alt_text() {
216 let content = r#"Here is an  image with no alt text.
217
218This  is fine.
219
220But this  is not.
221"#;
222
223 let document = create_test_document(content);
224 let rule = MD042;
225 let violations = rule.check(&document).unwrap();
226 assert_eq!(violations.len(), 2);
227 assert!(violations[0].message.contains("empty alt text"));
228 assert_eq!(violations[0].line, 1);
229 assert_eq!(violations[1].line, 5);
230 }
231
232 #[test]
233 fn test_md042_mixed_valid_and_invalid() {
234 let content = r#"Good [link](http://example.com) here.
235
236Bad [](http://example.com) link here.
237
238Another good [link text](http://example.com).
239
240Another bad [](http://bad.com) link.
241
242 image.
243
244 bad image.
245"#;
246
247 let document = create_test_document(content);
248 let rule = MD042;
249 let violations = rule.check(&document).unwrap();
250 assert_eq!(violations.len(), 3); let link_violations: Vec<_> = violations
254 .iter()
255 .filter(|v| v.message.contains("Found empty link"))
256 .collect();
257 let image_violations: Vec<_> = violations
258 .iter()
259 .filter(|v| v.message.contains("empty alt text"))
260 .collect();
261
262 assert_eq!(link_violations.len(), 2);
263 assert_eq!(image_violations.len(), 1);
264 }
265
266 #[test]
267 fn test_md042_autolinks_valid() {
268 let content = r#"Autolinks like <http://example.com> are fine.
269
270Email autolinks <user@example.com> are also okay.
271
272Regular [text links](http://example.com) work too.
273"#;
274
275 let document = create_test_document(content);
276 let rule = MD042;
277 let violations = rule.check(&document).unwrap();
278 assert_eq!(violations.len(), 0);
279 }
280
281 #[test]
282 fn test_md042_nested_formatting_valid() {
283 let content = r#"Complex [**bold _and italic_**](http://example.com) link.
284
285With [`code` and *emphasis*](http://example.com) mixed.
286"#;
287
288 let document = create_test_document(content);
289 let rule = MD042;
290 let violations = rule.check(&document).unwrap();
291 assert_eq!(violations.len(), 0);
292 }
293
294 #[test]
295 fn test_md042_reference_style_links() {
296 let content = r#"Good [reference link][good] here.
297
298Bad [][bad] reference link.
299
300[good]: http://example.com
301[bad]: http://example.com
302"#;
303
304 let document = create_test_document(content);
305 let rule = MD042;
306 let violations = rule.check(&document).unwrap();
307 assert_eq!(violations.len(), 1);
308 assert_eq!(violations[0].line, 3);
309 }
310}