mdlint/lint/rules/
md044.rs1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use regex::Regex;
5use serde_json::Value;
6
7pub struct MD044;
8
9impl Rule for MD044 {
10 fn name(&self) -> &str {
11 "MD044"
12 }
13
14 fn description(&self) -> &str {
15 "Proper names should have the correct capitalization"
16 }
17
18 fn tags(&self) -> &[&str] {
19 &["spelling"]
20 }
21
22 fn check(&self, parser: &MarkdownParser, config: Option<&Value>) -> Vec<Violation> {
23 let names = config
24 .and_then(|c| c.get("names"))
25 .and_then(|v| v.as_array())
26 .map(|arr| {
27 arr.iter()
28 .filter_map(|v| v.as_str().map(|s| s.to_string()))
29 .collect::<Vec<_>>()
30 });
31
32 let code_blocks = config
33 .and_then(|c| c.get("code_blocks"))
34 .and_then(|v| v.as_bool())
35 .unwrap_or(true);
36
37 let proper_names = match names {
39 Some(n) if !n.is_empty() => n,
40 _ => return Vec::new(),
41 };
42
43 let mut violations = Vec::new();
44
45 for (line_num, line) in parser.lines().iter().enumerate() {
46 let line_number = line_num + 1;
47
48 if !code_blocks
50 && (line.starts_with(" ") || line.starts_with('\t') || line.contains("```"))
51 {
52 continue;
53 }
54
55 for name in &proper_names {
57 let pattern = format!(r"(?i)\b{}\b", regex::escape(name));
59 if let Ok(re) = Regex::new(&pattern) {
60 for mat in re.find_iter(line) {
61 let found = mat.as_str();
62 if found != name {
64 violations.push(Violation {
65 line: line_number,
66 column: Some(mat.start() + 1),
67 rule: self.name().to_string(),
68 message: format!(
69 "Proper name '{}' should be capitalized as '{}'",
70 found, name
71 ),
72 fix: None,
73 });
74 }
75 }
76 }
77 }
78 }
79
80 violations
81 }
82
83 fn fixable(&self) -> bool {
84 false
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_no_config() {
94 let content = "This mentions javascript and rust";
95 let parser = MarkdownParser::new(content);
96 let rule = MD044;
97 let violations = rule.check(&parser, None);
98
99 assert_eq!(violations.len(), 0); }
101
102 #[test]
103 fn test_correct_capitalization() {
104 let content = "We use JavaScript and TypeScript.";
105 let parser = MarkdownParser::new(content);
106 let rule = MD044;
107 let config = serde_json::json!({
108 "names": ["JavaScript", "TypeScript"]
109 });
110 let violations = rule.check(&parser, Some(&config));
111
112 assert_eq!(violations.len(), 0);
113 }
114
115 #[test]
116 fn test_incorrect_capitalization() {
117 let content = "We use javascript and typescript.";
118 let parser = MarkdownParser::new(content);
119 let rule = MD044;
120 let config = serde_json::json!({
121 "names": ["JavaScript", "TypeScript"]
122 });
123 let violations = rule.check(&parser, Some(&config));
124
125 assert_eq!(violations.len(), 2);
126 }
127
128 #[test]
129 fn test_partial_match() {
130 let content = "JavaScriptCore is different from JavaScript";
131 let parser = MarkdownParser::new(content);
132 let rule = MD044;
133 let config = serde_json::json!({
134 "names": ["JavaScript"]
135 });
136 let violations = rule.check(&parser, Some(&config));
137
138 assert_eq!(violations.len(), 0);
140 }
141}