mdbook_lint_core/rules/standard/
md037.rs

1use crate::Document;
2use crate::error::Result;
3use crate::rule::{Rule, RuleCategory, RuleMetadata};
4use crate::violation::{Severity, Violation};
5
6/// MD037 - Spaces inside emphasis markers
7pub 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        // Look for patterns like "** text **", "* text *", etc.
15        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            // Check if we found the opening marker
36            if chars[i..i + marker_len] == marker_chars {
37                // Look for closing marker
38                let mut j = i + marker_len;
39                while j + marker_len <= chars.len() {
40                    if chars[j..j + marker_len] == marker_chars {
41                        // Found a pair, check for spaces
42                        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                // Make sure this isn't part of a double marker
86                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                // Look for closing marker
94                let mut j = i + 1;
95                while j < chars.len() {
96                    if chars[j] == marker {
97                        // Make sure this isn't part of a double marker
98                        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                        // Found a pair, check for spaces
106                        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); // ** bold **
246        assert_eq!(violations[1].line, 1); // * italic *
247        assert_eq!(violations[2].line, 5); // __bold __
248        assert_eq!(violations[3].line, 5); // _italic _
249    }
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}