mdlint/lint/rules/
md050.rs1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::{Fix, Violation};
4use serde_json::Value;
5
6pub struct MD050;
7
8impl Rule for MD050 {
9 fn name(&self) -> &str {
10 "MD050"
11 }
12
13 fn description(&self) -> &str {
14 "Strong style should be consistent"
15 }
16
17 fn tags(&self) -> &[&str] {
18 &["emphasis"]
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("asterisk");
26
27 let mut violations = Vec::new();
28 let mut first_style: Option<&str> = None;
29
30 let code_ranges = parser.get_code_ranges();
32
33 let is_in_code = |line_num: usize, byte_offset: usize| -> bool {
35 let absolute_offset = parser.line_offset_to_absolute(line_num, byte_offset);
36 code_ranges
37 .iter()
38 .any(|range| range.contains(&absolute_offset))
39 };
40
41 for (line_num, line) in parser.lines().iter().enumerate() {
42 let line_number = line_num + 1;
43
44 let chars: Vec<char> = line.chars().collect();
46 let mut i = 0;
47
48 while i + 1 < chars.len() {
49 if i + 1 < chars.len() {
51 let two_char = format!("{}{}", chars[i], chars[i + 1]);
52
53 if two_char == "**" || two_char == "__" {
54 let mut found_close = false;
56 for j in (i + 2)..chars.len().saturating_sub(1) {
57 if j + 1 < chars.len() {
58 let close_two = format!("{}{}", chars[j], chars[j + 1]);
59 if close_two == two_char {
60 if is_in_code(line_number, i) {
62 i = j; break;
64 }
65
66 found_close = true;
67
68 let current_style = if two_char == "**" {
70 "asterisk"
71 } else {
72 "underscore"
73 };
74
75 let make_fix = |col: usize, target: &str| Fix {
76 line_start: line_number,
77 line_end: line_number,
78 column_start: Some(col),
79 column_end: Some(col + 1),
80 replacement: target.to_string(),
81 description: "Replace strong marker".to_string(),
82 };
83
84 if style == "consistent" {
85 if let Some(first) = first_style {
86 if current_style != first {
87 let expected_marker =
88 if first == "asterisk" { "**" } else { "__" };
89 violations.push(Violation {
91 line: line_number,
92 column: Some(i + 1),
93 rule: self.name().to_string(),
94 message: format!(
95 "Strong style should be consistent: expected '{}', found '{}'",
96 expected_marker, two_char
97 ),
98 fix: Some(make_fix(i + 1, expected_marker)),
99 });
100 violations.push(Violation {
101 line: line_number,
102 column: Some(j + 1),
103 rule: self.name().to_string(),
104 message: format!(
105 "Strong style should be consistent: expected '{}', found '{}'",
106 expected_marker, close_two
107 ),
108 fix: Some(make_fix(j + 1, expected_marker)),
109 });
110 }
111 } else {
112 first_style = Some(current_style);
113 }
114 } else {
115 let expected_marker =
116 if style == "asterisk" { "**" } else { "__" };
117 if two_char != expected_marker {
118 violations.push(Violation {
120 line: line_number,
121 column: Some(i + 1),
122 rule: self.name().to_string(),
123 message: format!(
124 "Strong style should be '{}', found '{}'",
125 expected_marker, two_char
126 ),
127 fix: Some(make_fix(i + 1, expected_marker)),
128 });
129 violations.push(Violation {
130 line: line_number,
131 column: Some(j + 1),
132 rule: self.name().to_string(),
133 message: format!(
134 "Strong style should be '{}', found '{}'",
135 expected_marker, close_two
136 ),
137 fix: Some(make_fix(j + 1, expected_marker)),
138 });
139 }
140 }
141
142 i = j + 1; break;
144 }
145 }
146 }
147
148 if found_close {
149 i += 1;
150 continue;
151 }
152 }
153 }
154
155 i += 1;
156 }
157 }
158
159 violations
160 }
161
162 fn fixable(&self) -> bool {
163 true
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_consistent_asterisk() {
173 let content = "This is **bold** and **more bold**.";
174 let parser = MarkdownParser::new(content);
175 let rule = MD050;
176 let violations = rule.check(&parser, None);
177
178 assert_eq!(violations.len(), 0);
179 }
180
181 #[test]
182 fn test_consistent_underscore() {
183 let content = "This is __bold__ and __more bold__.";
184 let parser = MarkdownParser::new(content);
185 let rule = MD050;
186 let config = serde_json::json!({ "style": "consistent" });
187 let violations = rule.check(&parser, Some(&config));
188
189 assert_eq!(violations.len(), 0);
190 }
191
192 #[test]
193 fn test_inconsistent() {
194 let content = "This is **bold** and __also bold__.";
195 let parser = MarkdownParser::new(content);
196 let rule = MD050;
197 let violations = rule.check(&parser, None);
198
199 assert_eq!(violations.len(), 2);
201 }
202
203 #[test]
204 fn test_enforced_style() {
205 let content = "This is __bold__ text.";
206 let parser = MarkdownParser::new(content);
207 let rule = MD050;
208 let config = serde_json::json!({ "style": "asterisk" });
209 let violations = rule.check(&parser, Some(&config));
210
211 assert_eq!(violations.len(), 2);
213 }
214
215 #[test]
216 fn test_code_block_with_underscores() {
217 let content = "Some **bold** text.\n\n\
218 ```txt\n__tests__\n```\n\n\
219 More **bold** text.";
220 let parser = MarkdownParser::new(content);
221 let rule = MD050;
222 let violations = rule.check(&parser, None);
223
224 assert_eq!(violations.len(), 0);
226 }
227
228 #[test]
229 fn test_inline_code_with_underscores() {
230 let content = "Some `__code__`, **bold** text and `__code__`.";
231 let parser = MarkdownParser::new(content);
232 let rule = MD050;
233 let violations = rule.check(&parser, None);
234
235 assert_eq!(violations.len(), 0);
237 }
238}