mdbook_lint_core/rules/standard/
md046.rs1use crate::error::Result;
6use crate::rule::{AstRule, RuleCategory, RuleMetadata};
7use crate::{
8 Document,
9 violation::{Severity, Violation},
10};
11use comrak::nodes::{AstNode, NodeValue};
12
13pub struct MD046 {
15 style: CodeBlockStyle,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum CodeBlockStyle {
21 Fenced,
23 Indented,
25 Consistent,
27}
28
29impl MD046 {
30 pub fn new() -> Self {
32 Self {
33 style: CodeBlockStyle::Consistent,
34 }
35 }
36
37 #[allow(dead_code)]
39 pub fn with_style(style: CodeBlockStyle) -> Self {
40 Self { style }
41 }
42
43 fn get_code_block_style(&self, node: &AstNode) -> Option<CodeBlockStyle> {
45 if let NodeValue::CodeBlock(code_block) = &node.data.borrow().value {
46 if code_block.fenced {
48 Some(CodeBlockStyle::Fenced)
49 } else {
50 Some(CodeBlockStyle::Indented)
51 }
52 } else {
53 None
54 }
55 }
56
57 fn get_position<'a>(&self, node: &'a AstNode<'a>) -> (usize, usize) {
59 let data = node.data.borrow();
60 let pos = data.sourcepos;
61 (pos.start.line, pos.start.column)
62 }
63
64 fn check_node<'a>(
66 &self,
67 node: &'a AstNode<'a>,
68 violations: &mut Vec<Violation>,
69 expected_style: &mut Option<CodeBlockStyle>,
70 ) {
71 if let NodeValue::CodeBlock(_) = &node.data.borrow().value
72 && let Some(current_style) = self.get_code_block_style(node)
73 {
74 if let Some(expected) = expected_style {
75 if *expected != current_style {
77 let (line, column) = self.get_position(node);
78 let expected_name = match expected {
79 CodeBlockStyle::Fenced => "fenced",
80 CodeBlockStyle::Indented => "indented",
81 CodeBlockStyle::Consistent => "consistent", };
83 let found_name = match current_style {
84 CodeBlockStyle::Fenced => "fenced",
85 CodeBlockStyle::Indented => "indented",
86 CodeBlockStyle::Consistent => "consistent", };
88
89 violations.push(self.create_violation(
90 format!(
91 "Code block style inconsistent - expected {expected_name} but found {found_name}"
92 ),
93 line,
94 column,
95 Severity::Warning,
96 ));
97 }
98 } else {
99 match self.style {
101 CodeBlockStyle::Fenced => *expected_style = Some(CodeBlockStyle::Fenced),
102 CodeBlockStyle::Indented => *expected_style = Some(CodeBlockStyle::Indented),
103 CodeBlockStyle::Consistent => *expected_style = Some(current_style),
104 }
105 }
106 }
107
108 for child in node.children() {
110 self.check_node(child, violations, expected_style);
111 }
112 }
113}
114
115impl Default for MD046 {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121impl AstRule for MD046 {
122 fn id(&self) -> &'static str {
123 "MD046"
124 }
125
126 fn name(&self) -> &'static str {
127 "code-block-style"
128 }
129
130 fn description(&self) -> &'static str {
131 "Code block style should be consistent"
132 }
133
134 fn metadata(&self) -> RuleMetadata {
135 RuleMetadata::stable(RuleCategory::Formatting).introduced_in("mdbook-lint v0.1.0")
136 }
137
138 fn check_ast<'a>(&self, _document: &Document, ast: &'a AstNode<'a>) -> Result<Vec<Violation>> {
139 let mut violations = Vec::new();
140 let mut expected_style = match self.style {
141 CodeBlockStyle::Fenced => Some(CodeBlockStyle::Fenced),
142 CodeBlockStyle::Indented => Some(CodeBlockStyle::Indented),
143 CodeBlockStyle::Consistent => None, };
145
146 self.check_node(ast, &mut violations, &mut expected_style);
147 Ok(violations)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::rule::Rule;
155 use std::path::PathBuf;
156
157 fn create_test_document(content: &str) -> Document {
158 Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
159 }
160
161 #[test]
162 fn test_md046_consistent_fenced_style() {
163 let content = r#"Here is some fenced code:
164
165```rust
166fn main() {
167 println!("Hello");
168}
169```
170
171And another fenced block:
172
173```python
174print("Hello")
175```
176"#;
177
178 let document = create_test_document(content);
179 let rule = MD046::new();
180 let violations = rule.check(&document).unwrap();
181 assert_eq!(violations.len(), 0);
182 }
183
184 #[test]
185 fn test_md046_consistent_indented_style() {
186 let content = r#"Here is some indented code:
187
188 fn main() {
189 println!("Hello");
190 }
191
192And another indented block:
193
194 print("Hello")
195"#;
196
197 let document = create_test_document(content);
198 let rule = MD046::new();
199 let violations = rule.check(&document).unwrap();
200 assert_eq!(violations.len(), 0);
201 }
202
203 #[test]
204 fn test_md046_mixed_styles_violation() {
205 let content = r#"Here is fenced code:
206
207```rust
208fn main() {
209 println!("Hello");
210}
211```
212
213And here is indented code:
214
215 print("Hello")
216"#;
217
218 let document = create_test_document(content);
219 let rule = MD046::new();
220 let violations = rule.check(&document).unwrap();
221 assert_eq!(violations.len(), 1);
222 assert_eq!(violations[0].rule_id, "MD046");
223 assert!(
224 violations[0]
225 .message
226 .contains("expected fenced but found indented")
227 );
228 }
229
230 #[test]
231 fn test_md046_preferred_fenced_style() {
232 let content = r#"Here is indented code:
233
234 print("Hello")
235"#;
236
237 let document = create_test_document(content);
238 let rule = MD046::with_style(CodeBlockStyle::Fenced);
239 let violations = rule.check(&document).unwrap();
240 assert_eq!(violations.len(), 1);
241 assert!(
242 violations[0]
243 .message
244 .contains("expected fenced but found indented")
245 );
246 }
247
248 #[test]
249 fn test_md046_preferred_indented_style() {
250 let content = r#"Here is fenced code:
251
252```rust
253fn main() {}
254```
255"#;
256
257 let document = create_test_document(content);
258 let rule = MD046::with_style(CodeBlockStyle::Indented);
259 let violations = rule.check(&document).unwrap();
260 assert_eq!(violations.len(), 1);
261 assert!(
262 violations[0]
263 .message
264 .contains("expected indented but found fenced")
265 );
266 }
267
268 #[test]
269 fn test_md046_multiple_fenced_blocks() {
270 let content = r#"First block:
271
272```rust
273fn main() {}
274```
275
276Second block:
277
278```python
279print("hello")
280```
281
282Third block:
283
284```javascript
285console.log("hello");
286```
287"#;
288
289 let document = create_test_document(content);
290 let rule = MD046::new();
291 let violations = rule.check(&document).unwrap();
292 assert_eq!(violations.len(), 0);
293 }
294
295 #[test]
296 fn test_md046_multiple_indented_blocks() {
297 let content = r#"First block:
298
299 fn main() {}
300
301Second block:
302
303 print("hello")
304
305Third block:
306
307 console.log("hello");
308"#;
309
310 let document = create_test_document(content);
311 let rule = MD046::new();
312 let violations = rule.check(&document).unwrap();
313 assert_eq!(violations.len(), 0);
314 }
315
316 #[test]
317 fn test_md046_mixed_multiple_violations() {
318 let content = r#"Start with fenced:
319
320```rust
321fn main() {}
322```
323
324Then indented:
325
326 print("hello")
327
328Then fenced again:
329
330```javascript
331console.log("hello");
332```
333
334And indented again:
335
336 another_function()
337"#;
338
339 let document = create_test_document(content);
340 let rule = MD046::new();
341 let violations = rule.check(&document).unwrap();
342 assert_eq!(violations.len(), 2); assert!(
344 violations[0]
345 .message
346 .contains("expected fenced but found indented")
347 );
348 assert!(
349 violations[1]
350 .message
351 .contains("expected fenced but found indented")
352 );
353 }
354
355 #[test]
356 fn test_md046_no_code_blocks() {
357 let content = r#"This document has no code blocks.
358
359Just regular text and paragraphs.
360
361And maybe some `inline code` but no blocks.
362"#;
363
364 let document = create_test_document(content);
365 let rule = MD046::new();
366 let violations = rule.check(&document).unwrap();
367 assert_eq!(violations.len(), 0);
368 }
369
370 #[test]
371 fn test_md046_tilde_fenced_blocks() {
372 let content = r#"Using tilde fences:
373
374~~~rust
375fn main() {}
376~~~
377
378And backtick fences:
379
380```python
381print("hello")
382```
383"#;
384
385 let document = create_test_document(content);
386 let rule = MD046::new();
387 let violations = rule.check(&document).unwrap();
388 assert_eq!(violations.len(), 0);
390 }
391
392 #[test]
393 fn test_md046_fenced_vs_indented_first_determines() {
394 let content = r#"Start with indented:
395
396 fn main() {}
397
398Then fenced should be flagged:
399
400```python
401print("hello")
402```
403"#;
404
405 let document = create_test_document(content);
406 let rule = MD046::new();
407 let violations = rule.check(&document).unwrap();
408 assert_eq!(violations.len(), 1);
409 assert!(
410 violations[0]
411 .message
412 .contains("expected indented but found fenced")
413 );
414 }
415}