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) -> &'static str {
10 "MD048"
11 }
12
13 fn description(&self) -> &'static 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_owned(),
47 message: format!(
48 "Code fence style should be consistent: expected '{first}', found '{fence_char}'"
49 ),
50 fix: None,
51 });
52 }
53 } else {
54 first_style = Some(fence_char);
55 }
56 } else if style == "tilde" {
57 violations.push(Violation {
58 line: line_number,
59 column: Some(1),
60 rule: self.name().to_owned(),
61 message: "Code fence style should be 'tilde' (~), found backtick (`)"
62 .to_owned(),
63 fix: None,
64 });
65 }
66 }
67 in_code_block = !in_code_block;
68 } else if trimmed.starts_with("~~~") {
69 if !in_code_block {
71 let fence_char = '~';
72 if style == "consistent" {
73 if let Some(first) = first_style {
74 if fence_char != first {
75 violations.push(Violation {
76 line: line_number,
77 column: Some(1),
78 rule: self.name().to_owned(),
79 message: format!(
80 "Code fence style should be consistent: expected '{first}', found '{fence_char}'"
81 ),
82 fix: None,
83 });
84 }
85 } else {
86 first_style = Some(fence_char);
87 }
88 } else if style == "backtick" {
89 violations.push(Violation {
90 line: line_number,
91 column: Some(1),
92 rule: self.name().to_owned(),
93 message: "Code fence style should be 'backtick' (`), found tilde (~)"
94 .to_owned(),
95 fix: None,
96 });
97 }
98 }
99 in_code_block = !in_code_block;
100 }
101 }
102
103 violations
104 }
105
106 fn fixable(&self) -> bool {
107 false
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn test_consistent_backtick() {
117 let content = "```\ncode1\n```\n\n```\ncode2\n```";
118 let parser = MarkdownParser::new(content);
119 let rule = MD048;
120 let violations = rule.check(&parser, None);
121
122 assert_eq!(violations.len(), 0);
123 }
124
125 #[test]
126 fn test_consistent_tilde() {
127 let content = "~~~\ncode1\n~~~\n\n~~~\ncode2\n~~~";
128 let parser = MarkdownParser::new(content);
129 let rule = MD048;
130 let config = serde_json::json!({ "style": "consistent" });
131 let violations = rule.check(&parser, Some(&config));
132
133 assert_eq!(violations.len(), 0);
134 }
135
136 #[test]
137 fn test_inconsistent() {
138 let content = "```\ncode1\n```\n\n~~~\ncode2\n~~~";
139 let parser = MarkdownParser::new(content);
140 let rule = MD048;
141 let violations = rule.check(&parser, None);
142
143 assert_eq!(violations.len(), 1); }
145
146 #[test]
147 fn test_enforced_backtick() {
148 let content = "~~~\ncode\n~~~";
149 let parser = MarkdownParser::new(content);
150 let rule = MD048;
151 let config = serde_json::json!({ "style": "backtick" });
152 let violations = rule.check(&parser, Some(&config));
153
154 assert_eq!(violations.len(), 1); }
156}