Skip to main content

mdlint/lint/rules/
md059.rs

1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use pulldown_cmark::{Event, Tag, TagEnd};
5use serde_json::Value;
6
7pub struct MD059;
8
9impl Rule for MD059 {
10    fn name(&self) -> &str {
11        "MD059"
12    }
13
14    fn description(&self) -> &str {
15        "Link text should be descriptive"
16    }
17
18    fn tags(&self) -> &[&str] {
19        &["links", "accessibility"]
20    }
21
22    fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
23        let mut violations = Vec::new();
24
25        // List of non-descriptive link texts
26        let non_descriptive = [
27            "click here",
28            "here",
29            "link",
30            "read more",
31            "more",
32            "this",
33            "this link",
34            "click",
35        ];
36
37        let mut in_link = false;
38        let mut link_text = String::new();
39        let mut link_line = 0;
40
41        for (event, range) in parser.parse_with_offsets() {
42            match event {
43                Event::Start(Tag::Link { .. }) => {
44                    in_link = true;
45                    link_text.clear();
46                    link_line = parser.offset_to_line(range.start);
47                }
48                Event::Text(text) if in_link => {
49                    link_text.push_str(&text);
50                }
51                Event::End(TagEnd::Link) if in_link => {
52                    let text_lower = link_text.trim().to_lowercase();
53
54                    // Check if link text is non-descriptive
55                    if non_descriptive.contains(&text_lower.as_str()) {
56                        violations.push(Violation {
57                            line: link_line,
58                            column: Some(1),
59                            rule: self.name().to_string(),
60                            message: format!(
61                                "Link text '{}' is not descriptive; use meaningful text",
62                                link_text.trim()
63                            ),
64                            fix: None,
65                        });
66                    }
67
68                    in_link = false;
69                }
70                _ => {}
71            }
72        }
73
74        violations
75    }
76
77    fn fixable(&self) -> bool {
78        false
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_descriptive_link() {
88        let content = "See the [documentation](https://example.com) for details.";
89        let parser = MarkdownParser::new(content);
90        let rule = MD059;
91        let violations = rule.check(&parser, None);
92
93        assert_eq!(violations.len(), 0);
94    }
95
96    #[test]
97    fn test_click_here() {
98        let content = "[Click here](https://example.com) to continue.";
99        let parser = MarkdownParser::new(content);
100        let rule = MD059;
101        let violations = rule.check(&parser, None);
102
103        assert_eq!(violations.len(), 1);
104        assert!(violations[0].message.contains("Click here"));
105    }
106
107    #[test]
108    fn test_here() {
109        let content = "You can find it [here](https://example.com).";
110        let parser = MarkdownParser::new(content);
111        let rule = MD059;
112        let violations = rule.check(&parser, None);
113
114        assert_eq!(violations.len(), 1);
115    }
116
117    #[test]
118    fn test_multiple_bad_links() {
119        let content = "[Click here](url1) and [read more](url2).";
120        let parser = MarkdownParser::new(content);
121        let rule = MD059;
122        let violations = rule.check(&parser, None);
123
124        assert_eq!(violations.len(), 2);
125    }
126
127    #[test]
128    fn test_case_insensitive() {
129        let content = "[CLICK HERE](https://example.com) is bad.";
130        let parser = MarkdownParser::new(content);
131        let rule = MD059;
132        let violations = rule.check(&parser, None);
133
134        assert_eq!(violations.len(), 1);
135    }
136}