Skip to main content

mdlint/lint/rules/
md048.rs

1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use serde_json::Value;
5
6pub struct MD048;
7
8impl Rule for MD048 {
9    fn name(&self) -> &str {
10        "MD048"
11    }
12
13    fn description(&self) -> &str {
14        "Code fence style"
15    }
16
17    fn tags(&self) -> &[&str] {
18        &["code"]
19    }
20
21    fn check(&self, parser: &MarkdownParser, config: Option<&Value>) -> Vec<Violation> {
22        let style = config
23            .and_then(|c| c.get("style"))
24            .and_then(|v| v.as_str())
25            .unwrap_or("backtick");
26
27        let mut violations = Vec::new();
28        let mut first_style: Option<char> = None;
29        let mut in_code_block = false;
30
31        for (line_num, line) in parser.lines().iter().enumerate() {
32            let line_number = line_num + 1;
33            let trimmed = line.trim();
34
35            // Check if line is a code fence (opening or closing)
36            if trimmed.starts_with("```") {
37                // Only check opening fence, not closing
38                if !in_code_block {
39                    let fence_char = '`';
40                    if style == "consistent" {
41                        if let Some(first) = first_style {
42                            if fence_char != first {
43                                violations.push(Violation {
44                                    line: line_number,
45                                    column: Some(1),
46                                    rule: self.name().to_string(),
47                                    message: format!(
48                                        "Code fence style should be consistent: expected '{}', found '{}'",
49                                        first, fence_char
50                                    ),
51                                    fix: None,
52                                });
53                            }
54                        } else {
55                            first_style = Some(fence_char);
56                        }
57                    } else if style == "tilde" {
58                        violations.push(Violation {
59                            line: line_number,
60                            column: Some(1),
61                            rule: self.name().to_string(),
62                            message: "Code fence style should be 'tilde' (~), found backtick (`)"
63                                .to_string(),
64                            fix: None,
65                        });
66                    }
67                }
68                in_code_block = !in_code_block;
69            } else if trimmed.starts_with("~~~") {
70                // Only check opening fence, not closing
71                if !in_code_block {
72                    let fence_char = '~';
73                    if style == "consistent" {
74                        if let Some(first) = first_style {
75                            if fence_char != first {
76                                violations.push(Violation {
77                                    line: line_number,
78                                    column: Some(1),
79                                    rule: self.name().to_string(),
80                                    message: format!(
81                                        "Code fence style should be consistent: expected '{}', found '{}'",
82                                        first, fence_char
83                                    ),
84                                    fix: None,
85                                });
86                            }
87                        } else {
88                            first_style = Some(fence_char);
89                        }
90                    } else if style == "backtick" {
91                        violations.push(Violation {
92                            line: line_number,
93                            column: Some(1),
94                            rule: self.name().to_string(),
95                            message: "Code fence style should be 'backtick' (`), found tilde (~)"
96                                .to_string(),
97                            fix: None,
98                        });
99                    }
100                }
101                in_code_block = !in_code_block;
102            }
103        }
104
105        violations
106    }
107
108    fn fixable(&self) -> bool {
109        false
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_consistent_backtick() {
119        let content = "```\ncode1\n```\n\n```\ncode2\n```";
120        let parser = MarkdownParser::new(content);
121        let rule = MD048;
122        let violations = rule.check(&parser, None);
123
124        assert_eq!(violations.len(), 0);
125    }
126
127    #[test]
128    fn test_consistent_tilde() {
129        let content = "~~~\ncode1\n~~~\n\n~~~\ncode2\n~~~";
130        let parser = MarkdownParser::new(content);
131        let rule = MD048;
132        let config = serde_json::json!({ "style": "consistent" });
133        let violations = rule.check(&parser, Some(&config));
134
135        assert_eq!(violations.len(), 0);
136    }
137
138    #[test]
139    fn test_inconsistent() {
140        let content = "```\ncode1\n```\n\n~~~\ncode2\n~~~";
141        let parser = MarkdownParser::new(content);
142        let rule = MD048;
143        let violations = rule.check(&parser, None);
144
145        assert_eq!(violations.len(), 1); // Only opening of second block
146    }
147
148    #[test]
149    fn test_enforced_backtick() {
150        let content = "~~~\ncode\n~~~";
151        let parser = MarkdownParser::new(content);
152        let rule = MD048;
153        let config = serde_json::json!({ "style": "backtick" });
154        let violations = rule.check(&parser, Some(&config));
155
156        assert_eq!(violations.len(), 1); // Only opening
157    }
158}