mdlint/lint/rules/
md048.rs1use 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 if trimmed.starts_with("```") {
37 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 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); }
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); }
158}