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