Skip to main content

mdlint/lint/rules/
md044.rs

1use 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        // If no names are specified, skip check
38        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            // Skip code blocks if configured
49            if !code_blocks
50                && (line.starts_with("    ") || line.starts_with('\t') || line.contains("```"))
51            {
52                continue;
53            }
54
55            // Check each proper name
56            for name in &proper_names {
57                // Create case-insensitive regex with word boundaries
58                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                        // Check if capitalization matches
63                        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); // No names configured
100    }
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        // Should only match whole word "JavaScript", not "JavaScriptCore"
139        assert_eq!(violations.len(), 0);
140    }
141}