mdbook_lint_core/rules/standard/
md037.rs1use crate::Document;
2use crate::error::Result;
3use crate::rule::{Rule, RuleCategory, RuleMetadata};
4use crate::violation::{Severity, Violation};
5
6pub struct MD037;
8
9impl MD037 {
10 fn find_emphasis_violations(&self, line: &str, line_number: usize) -> Vec<Violation> {
11 let mut violations = Vec::new();
12 let chars: Vec<char> = line.chars().collect();
13
14 self.check_pattern(&chars, "**", line_number, &mut violations);
16 self.check_pattern(&chars, "__", line_number, &mut violations);
17 self.check_single_pattern(&chars, '*', line_number, &mut violations);
18 self.check_single_pattern(&chars, '_', line_number, &mut violations);
19
20 violations
21 }
22
23 fn check_pattern(
24 &self,
25 chars: &[char],
26 marker: &str,
27 line_number: usize,
28 violations: &mut Vec<Violation>,
29 ) {
30 let marker_chars: Vec<char> = marker.chars().collect();
31 let marker_len = marker_chars.len();
32 let mut i = 0;
33
34 while i + marker_len < chars.len() {
35 if chars[i..i + marker_len] == marker_chars {
37 let mut j = i + marker_len;
39 while j + marker_len <= chars.len() {
40 if chars[j..j + marker_len] == marker_chars {
41 let content_start = i + marker_len;
43 let content_end = j;
44
45 if content_start < content_end {
46 let has_leading_space = chars[content_start].is_whitespace();
47 let has_trailing_space = chars[content_end - 1].is_whitespace();
48
49 if has_leading_space || has_trailing_space {
50 violations.push(self.create_violation(
51 "Spaces inside emphasis markers".to_string(),
52 line_number,
53 i + 1,
54 Severity::Warning,
55 ));
56 }
57 }
58
59 i = j + marker_len;
60 break;
61 }
62 j += 1;
63 }
64
65 if j + marker_len > chars.len() {
66 i += 1;
67 }
68 } else {
69 i += 1;
70 }
71 }
72 }
73
74 fn check_single_pattern(
75 &self,
76 chars: &[char],
77 marker: char,
78 line_number: usize,
79 violations: &mut Vec<Violation>,
80 ) {
81 let mut i = 0;
82
83 while i < chars.len() {
84 if chars[i] == marker {
85 if (i > 0 && chars[i - 1] == marker)
87 || (i + 1 < chars.len() && chars[i + 1] == marker)
88 {
89 i += 1;
90 continue;
91 }
92
93 let mut j = i + 1;
95 while j < chars.len() {
96 if chars[j] == marker {
97 if (j > 0 && chars[j - 1] == marker)
99 || (j + 1 < chars.len() && chars[j + 1] == marker)
100 {
101 j += 1;
102 continue;
103 }
104
105 let content_start = i + 1;
107 let content_end = j;
108
109 if content_start < content_end {
110 let has_leading_space = chars[content_start].is_whitespace();
111 let has_trailing_space = chars[content_end - 1].is_whitespace();
112
113 if has_leading_space || has_trailing_space {
114 violations.push(self.create_violation(
115 "Spaces inside emphasis markers".to_string(),
116 line_number,
117 i + 1,
118 Severity::Warning,
119 ));
120 }
121 }
122
123 i = j + 1;
124 break;
125 }
126 j += 1;
127 }
128
129 if j >= chars.len() {
130 i += 1;
131 }
132 } else {
133 i += 1;
134 }
135 }
136 }
137}
138
139impl Rule for MD037 {
140 fn id(&self) -> &'static str {
141 "MD037"
142 }
143
144 fn name(&self) -> &'static str {
145 "no-space-in-emphasis"
146 }
147
148 fn description(&self) -> &'static str {
149 "Spaces inside emphasis markers"
150 }
151
152 fn metadata(&self) -> RuleMetadata {
153 RuleMetadata::stable(RuleCategory::Formatting)
154 }
155
156 fn check_with_ast<'a>(
157 &self,
158 document: &Document,
159 _ast: Option<&'a comrak::nodes::AstNode<'a>>,
160 ) -> Result<Vec<Violation>> {
161 let mut violations = Vec::new();
162 let lines = document.content.lines();
163
164 for (line_number, line) in lines.enumerate() {
165 let line_number = line_number + 1;
166 violations.extend(self.find_emphasis_violations(line, line_number));
167 }
168
169 Ok(violations)
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::Document;
177 use std::path::PathBuf;
178
179 #[test]
180 fn test_md037_no_violations() {
181 let content = r#"Here is some **bold** text.
182
183Here is some *italic* text.
184
185Here is some more __bold__ text.
186
187Here is some more _italic_ text.
188"#;
189
190 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
191 let rule = MD037;
192 let violations = rule.check(&document).unwrap();
193 assert_eq!(violations.len(), 0);
194 }
195
196 #[test]
197 fn test_md037_spaces_in_bold() {
198 let content = r#"Here is some ** bold ** text.
199
200Here is some __bold __ text.
201
202Here is some __ bold__ text.
203"#;
204
205 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
206 let rule = MD037;
207 let violations = rule.check(&document).unwrap();
208 assert_eq!(violations.len(), 3);
209 assert_eq!(violations[0].line, 1);
210 assert_eq!(violations[1].line, 3);
211 assert_eq!(violations[2].line, 5);
212 }
213
214 #[test]
215 fn test_md037_spaces_in_italic() {
216 let content = r#"Here is some * italic * text.
217
218Here is some _italic _ text.
219
220Here is some _ italic_ text.
221"#;
222
223 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
224 let rule = MD037;
225 let violations = rule.check(&document).unwrap();
226 assert_eq!(violations.len(), 3);
227 assert_eq!(violations[0].line, 1);
228 assert_eq!(violations[1].line, 3);
229 assert_eq!(violations[2].line, 5);
230 }
231
232 #[test]
233 fn test_md037_mixed_violations() {
234 let content = r#"Here is ** bold ** and * italic * text.
235
236Normal **bold** and *italic* are fine.
237
238But __bold __ and _italic _ are not.
239"#;
240
241 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
242 let rule = MD037;
243 let violations = rule.check(&document).unwrap();
244 assert_eq!(violations.len(), 4);
245 assert_eq!(violations[0].line, 1); assert_eq!(violations[1].line, 1); assert_eq!(violations[2].line, 5); assert_eq!(violations[3].line, 5); }
250
251 #[test]
252 fn test_md037_no_false_positives() {
253 let content = r#"This line has * asterisk but not emphasis.
254
255This line has ** two asterisks but not emphasis.
256
257This has *proper* emphasis.
258
259This has **proper** emphasis too.
260
261Math: 2 times 3 times 4 = 24.
262"#;
263
264 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
265 let rule = MD037;
266 let violations = rule.check(&document).unwrap();
267 assert_eq!(violations.len(), 0);
268 }
269
270 #[test]
271 fn test_md037_nested_emphasis() {
272 let content = r#"This has ** bold with *italic* inside ** which is wrong.
273
274This has **bold with *italic* inside** which is correct.
275"#;
276
277 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
278 let rule = MD037;
279 let violations = rule.check(&document).unwrap();
280 assert_eq!(violations.len(), 1);
281 assert_eq!(violations[0].line, 1);
282 }
283
284 #[test]
285 fn test_md037_emphasis_at_line_boundaries() {
286 let content = r#"** bold at start **
287
288**bold at end **
289
290* italic at start *
291
292*italic at end *
293"#;
294
295 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
296 let rule = MD037;
297 let violations = rule.check(&document).unwrap();
298 assert_eq!(violations.len(), 4);
299 assert_eq!(violations[0].line, 1);
300 assert_eq!(violations[1].line, 3);
301 assert_eq!(violations[2].line, 5);
302 assert_eq!(violations[3].line, 7);
303 }
304
305 #[test]
306 fn test_md037_multiple_spaces() {
307 let content = r#"Here is some ** bold with multiple spaces ** text.
308
309Here is some * italic with multiple spaces * text.
310"#;
311
312 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
313 let rule = MD037;
314 let violations = rule.check(&document).unwrap();
315 assert_eq!(violations.len(), 2);
316 assert_eq!(violations[0].line, 1);
317 assert_eq!(violations[1].line, 3);
318 }
319}