mdlint/lint/rules/
md059.rs1use 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 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 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}