1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, Severity};
2use crate::rules::code_fence_utils::CodeFenceStyle;
3use crate::utils::range_utils::calculate_match_range;
4use toml;
5
6mod md048_config;
7use md048_config::MD048Config;
8
9#[derive(Debug, Clone, Copy)]
11struct FenceMarker<'a> {
12 fence_char: char,
14 fence_len: usize,
16 fence_start: usize,
18 rest: &'a str,
20}
21
22#[inline]
28fn parse_fence_marker(line: &str) -> Option<FenceMarker<'_>> {
29 let bytes = line.as_bytes();
30 let mut pos = 0usize;
31 while pos < bytes.len() && bytes[pos] == b' ' {
32 pos += 1;
33 }
34 if pos > 3 {
35 return None;
36 }
37
38 let fence_char = match bytes.get(pos).copied() {
39 Some(b'`') => '`',
40 Some(b'~') => '~',
41 _ => return None,
42 };
43
44 let marker = if fence_char == '`' { b'`' } else { b'~' };
45 let mut end = pos;
46 while end < bytes.len() && bytes[end] == marker {
47 end += 1;
48 }
49 let fence_len = end - pos;
50 if fence_len < 3 {
51 return None;
52 }
53
54 Some(FenceMarker {
55 fence_char,
56 fence_len,
57 fence_start: pos,
58 rest: &line[end..],
59 })
60}
61
62#[inline]
63fn is_closing_fence(marker: FenceMarker<'_>, opening_fence_char: char, opening_fence_len: usize) -> bool {
64 marker.fence_char == opening_fence_char && marker.fence_len >= opening_fence_len && marker.rest.trim().is_empty()
65}
66
67#[derive(Clone)]
71pub struct MD048CodeFenceStyle {
72 config: MD048Config,
73}
74
75impl MD048CodeFenceStyle {
76 pub fn new(style: CodeFenceStyle) -> Self {
77 Self {
78 config: MD048Config { style },
79 }
80 }
81
82 pub fn from_config_struct(config: MD048Config) -> Self {
83 Self { config }
84 }
85
86 fn detect_style(&self, ctx: &crate::lint_context::LintContext) -> Option<CodeFenceStyle> {
87 let mut backtick_count = 0;
89 let mut tilde_count = 0;
90 let mut in_code_block = false;
91 let mut opening_fence_char = '`';
92 let mut opening_fence_len = 0usize;
93
94 for line in ctx.content.lines() {
95 let Some(marker) = parse_fence_marker(line) else {
96 continue;
97 };
98
99 if !in_code_block {
100 if marker.fence_char == '`' {
102 backtick_count += 1;
103 } else {
104 tilde_count += 1;
105 }
106 in_code_block = true;
107 opening_fence_char = marker.fence_char;
108 opening_fence_len = marker.fence_len;
109 } else if is_closing_fence(marker, opening_fence_char, opening_fence_len) {
110 in_code_block = false;
111 }
112 }
113
114 if backtick_count >= tilde_count && backtick_count > 0 {
117 Some(CodeFenceStyle::Backtick)
118 } else if tilde_count > 0 {
119 Some(CodeFenceStyle::Tilde)
120 } else {
121 None
122 }
123 }
124}
125
126fn max_inner_fence_length_of_char(
144 lines: &[&str],
145 opening_line: usize,
146 opening_fence_len: usize,
147 opening_char: char,
148 target_char: char,
149) -> usize {
150 let mut max_len = 0usize;
151
152 for line in lines.iter().skip(opening_line + 1) {
153 let Some(marker) = parse_fence_marker(line) else {
154 continue;
155 };
156
157 if is_closing_fence(marker, opening_char, opening_fence_len) {
159 break;
160 }
161
162 if marker.fence_char == target_char && marker.rest.trim().is_empty() {
165 max_len = max_len.max(marker.fence_len);
166 }
167 }
168
169 max_len
170}
171
172impl Rule for MD048CodeFenceStyle {
173 fn name(&self) -> &'static str {
174 "MD048"
175 }
176
177 fn description(&self) -> &'static str {
178 "Code fence style should be consistent"
179 }
180
181 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
182 let content = ctx.content;
183 let line_index = &ctx.line_index;
184
185 let mut warnings = Vec::new();
186
187 let target_style = match self.config.style {
188 CodeFenceStyle::Consistent => self.detect_style(ctx).unwrap_or(CodeFenceStyle::Backtick),
189 _ => self.config.style,
190 };
191
192 let lines: Vec<&str> = content.lines().collect();
193 let mut in_code_block = false;
194 let mut code_block_fence_char = '`';
195 let mut code_block_fence_len = 0usize;
196 let mut converted_fence_len = 0usize;
199 let mut needs_lengthening = false;
202
203 for (line_num, &line) in lines.iter().enumerate() {
204 let Some(marker) = parse_fence_marker(line) else {
205 continue;
206 };
207 let fence_char = marker.fence_char;
208 let fence_len = marker.fence_len;
209
210 if !in_code_block {
211 in_code_block = true;
212 code_block_fence_char = fence_char;
213 code_block_fence_len = fence_len;
214
215 let needs_conversion = (fence_char == '`' && target_style == CodeFenceStyle::Tilde)
216 || (fence_char == '~' && target_style == CodeFenceStyle::Backtick);
217
218 if needs_conversion {
219 let target_char = if target_style == CodeFenceStyle::Backtick {
220 '`'
221 } else {
222 '~'
223 };
224
225 let prefix = &line[..marker.fence_start];
228 let info = marker.rest;
229 let max_inner =
230 max_inner_fence_length_of_char(&lines, line_num, fence_len, fence_char, target_char);
231 converted_fence_len = fence_len.max(max_inner + 1);
232 needs_lengthening = false;
233
234 let replacement = format!("{prefix}{}{info}", target_char.to_string().repeat(converted_fence_len));
235
236 let fence_start = marker.fence_start;
237 let fence_end = fence_start + fence_len;
238 let (start_line, start_col, end_line, end_col) =
239 calculate_match_range(line_num + 1, line, fence_start, fence_end - fence_start);
240
241 warnings.push(LintWarning {
242 rule_name: Some(self.name().to_string()),
243 message: format!(
244 "Code fence style: use {} instead of {}",
245 if target_style == CodeFenceStyle::Backtick {
246 "```"
247 } else {
248 "~~~"
249 },
250 if fence_char == '`' { "```" } else { "~~~" }
251 ),
252 line: start_line,
253 column: start_col,
254 end_line,
255 end_column: end_col,
256 severity: Severity::Warning,
257 fix: Some(Fix {
258 range: line_index.line_col_to_byte_range_with_length(line_num + 1, 1, line.len()),
259 replacement,
260 }),
261 });
262 } else {
263 let prefix = &line[..marker.fence_start];
268 let info = marker.rest;
269 let max_inner = max_inner_fence_length_of_char(&lines, line_num, fence_len, fence_char, fence_char);
270 if max_inner >= fence_len {
271 converted_fence_len = max_inner + 1;
272 needs_lengthening = true;
273
274 let replacement =
275 format!("{prefix}{}{info}", fence_char.to_string().repeat(converted_fence_len));
276
277 let fence_start = marker.fence_start;
278 let fence_end = fence_start + fence_len;
279 let (start_line, start_col, end_line, end_col) =
280 calculate_match_range(line_num + 1, line, fence_start, fence_end - fence_start);
281
282 warnings.push(LintWarning {
283 rule_name: Some(self.name().to_string()),
284 message: format!(
285 "Code fence length is ambiguous: outer fence ({fence_len} {}) \
286 contains interior fence sequences of equal length; \
287 use {converted_fence_len}",
288 if fence_char == '`' { "backticks" } else { "tildes" },
289 ),
290 line: start_line,
291 column: start_col,
292 end_line,
293 end_column: end_col,
294 severity: Severity::Warning,
295 fix: Some(Fix {
296 range: line_index.line_col_to_byte_range_with_length(line_num + 1, 1, line.len()),
297 replacement,
298 }),
299 });
300 } else {
301 converted_fence_len = fence_len;
302 needs_lengthening = false;
303 }
304 }
305 } else {
306 let is_closing = is_closing_fence(marker, code_block_fence_char, code_block_fence_len);
308
309 if is_closing {
310 let needs_conversion = (fence_char == '`' && target_style == CodeFenceStyle::Tilde)
311 || (fence_char == '~' && target_style == CodeFenceStyle::Backtick);
312
313 if needs_conversion || needs_lengthening {
314 let target_char = if needs_conversion {
315 if target_style == CodeFenceStyle::Backtick {
316 '`'
317 } else {
318 '~'
319 }
320 } else {
321 fence_char
322 };
323
324 let prefix = &line[..marker.fence_start];
325 let replacement = format!(
326 "{prefix}{}{}",
327 target_char.to_string().repeat(converted_fence_len),
328 marker.rest
329 );
330
331 let fence_start = marker.fence_start;
332 let fence_end = fence_start + fence_len;
333 let (start_line, start_col, end_line, end_col) =
334 calculate_match_range(line_num + 1, line, fence_start, fence_end - fence_start);
335
336 let message = if needs_conversion {
337 format!(
338 "Code fence style: use {} instead of {}",
339 if target_style == CodeFenceStyle::Backtick {
340 "```"
341 } else {
342 "~~~"
343 },
344 if fence_char == '`' { "```" } else { "~~~" }
345 )
346 } else {
347 format!(
348 "Code fence length is ambiguous: closing fence ({fence_len} {}) \
349 must match the lengthened outer fence; use {converted_fence_len}",
350 if fence_char == '`' { "backticks" } else { "tildes" },
351 )
352 };
353
354 warnings.push(LintWarning {
355 rule_name: Some(self.name().to_string()),
356 message,
357 line: start_line,
358 column: start_col,
359 end_line,
360 end_column: end_col,
361 severity: Severity::Warning,
362 fix: Some(Fix {
363 range: line_index.line_col_to_byte_range_with_length(line_num + 1, 1, line.len()),
364 replacement,
365 }),
366 });
367 }
368
369 in_code_block = false;
370 code_block_fence_len = 0;
371 converted_fence_len = 0;
372 needs_lengthening = false;
373 }
374 }
376 }
377
378 Ok(warnings)
379 }
380
381 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
383 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~'))
385 }
386
387 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
388 let content = ctx.content;
389
390 let target_style = match self.config.style {
391 CodeFenceStyle::Consistent => self.detect_style(ctx).unwrap_or(CodeFenceStyle::Backtick),
392 _ => self.config.style,
393 };
394
395 let lines: Vec<&str> = content.lines().collect();
396 let mut result = String::new();
397 let mut in_code_block = false;
398 let mut code_block_fence_char = '`';
399 let mut code_block_fence_len = 0usize;
400 let mut converted_fence_len = 0usize;
401 let mut needs_lengthening = false;
402
403 for (line_idx, &line) in lines.iter().enumerate() {
404 if let Some(marker) = parse_fence_marker(line) {
405 let fence_char = marker.fence_char;
406 let fence_len = marker.fence_len;
407
408 if !in_code_block {
409 in_code_block = true;
410 code_block_fence_char = fence_char;
411 code_block_fence_len = fence_len;
412
413 let needs_conversion = (fence_char == '`' && target_style == CodeFenceStyle::Tilde)
414 || (fence_char == '~' && target_style == CodeFenceStyle::Backtick);
415
416 let prefix = &line[..marker.fence_start];
417 let info = marker.rest;
418
419 if needs_conversion {
420 let target_char = if target_style == CodeFenceStyle::Backtick {
421 '`'
422 } else {
423 '~'
424 };
425
426 let max_inner =
427 max_inner_fence_length_of_char(&lines, line_idx, fence_len, fence_char, target_char);
428 converted_fence_len = fence_len.max(max_inner + 1);
429 needs_lengthening = false;
430
431 result.push_str(prefix);
432 result.push_str(&target_char.to_string().repeat(converted_fence_len));
433 result.push_str(info);
434 } else {
435 let max_inner =
437 max_inner_fence_length_of_char(&lines, line_idx, fence_len, fence_char, fence_char);
438 if max_inner >= fence_len {
439 converted_fence_len = max_inner + 1;
440 needs_lengthening = true;
441
442 result.push_str(prefix);
443 result.push_str(&fence_char.to_string().repeat(converted_fence_len));
444 result.push_str(info);
445 } else {
446 converted_fence_len = fence_len;
447 needs_lengthening = false;
448 result.push_str(line);
449 }
450 }
451 } else {
452 let is_closing = is_closing_fence(marker, code_block_fence_char, code_block_fence_len);
454
455 if is_closing {
456 let needs_conversion = (fence_char == '`' && target_style == CodeFenceStyle::Tilde)
457 || (fence_char == '~' && target_style == CodeFenceStyle::Backtick);
458
459 if needs_conversion || needs_lengthening {
460 let target_char = if needs_conversion {
461 if target_style == CodeFenceStyle::Backtick {
462 '`'
463 } else {
464 '~'
465 }
466 } else {
467 fence_char
468 };
469 let prefix = &line[..marker.fence_start];
470 result.push_str(prefix);
471 result.push_str(&target_char.to_string().repeat(converted_fence_len));
472 result.push_str(marker.rest);
473 } else {
474 result.push_str(line);
475 }
476
477 in_code_block = false;
478 code_block_fence_len = 0;
479 converted_fence_len = 0;
480 needs_lengthening = false;
481 } else {
482 result.push_str(line);
484 }
485 }
486 } else {
487 result.push_str(line);
488 }
489 result.push('\n');
490 }
491
492 if !content.ends_with('\n') && result.ends_with('\n') {
494 result.pop();
495 }
496
497 Ok(result)
498 }
499
500 fn as_any(&self) -> &dyn std::any::Any {
501 self
502 }
503
504 fn default_config_section(&self) -> Option<(String, toml::Value)> {
505 let json_value = serde_json::to_value(&self.config).ok()?;
506 Some((
507 self.name().to_string(),
508 crate::rule_config_serde::json_to_toml_value(&json_value)?,
509 ))
510 }
511
512 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
513 where
514 Self: Sized,
515 {
516 let rule_config = crate::rule_config_serde::load_rule_config::<MD048Config>(config);
517 Box::new(Self::from_config_struct(rule_config))
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use crate::lint_context::LintContext;
525
526 #[test]
527 fn test_backtick_style_with_backticks() {
528 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
529 let content = "```\ncode\n```";
530 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
531 let result = rule.check(&ctx).unwrap();
532
533 assert_eq!(result.len(), 0);
534 }
535
536 #[test]
537 fn test_backtick_style_with_tildes() {
538 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
539 let content = "~~~\ncode\n~~~";
540 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
541 let result = rule.check(&ctx).unwrap();
542
543 assert_eq!(result.len(), 2); assert!(result[0].message.contains("use ``` instead of ~~~"));
545 assert_eq!(result[0].line, 1);
546 assert_eq!(result[1].line, 3);
547 }
548
549 #[test]
550 fn test_tilde_style_with_tildes() {
551 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
552 let content = "~~~\ncode\n~~~";
553 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
554 let result = rule.check(&ctx).unwrap();
555
556 assert_eq!(result.len(), 0);
557 }
558
559 #[test]
560 fn test_tilde_style_with_backticks() {
561 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
562 let content = "```\ncode\n```";
563 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
564 let result = rule.check(&ctx).unwrap();
565
566 assert_eq!(result.len(), 2); assert!(result[0].message.contains("use ~~~ instead of ```"));
568 }
569
570 #[test]
571 fn test_consistent_style_tie_prefers_backtick() {
572 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
573 let content = "```\ncode\n```\n\n~~~\nmore code\n~~~";
575 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
576 let result = rule.check(&ctx).unwrap();
577
578 assert_eq!(result.len(), 2);
580 assert_eq!(result[0].line, 5);
581 assert_eq!(result[1].line, 7);
582 }
583
584 #[test]
585 fn test_consistent_style_tilde_most_prevalent() {
586 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
587 let content = "~~~\ncode\n~~~\n\n```\nmore code\n```\n\n~~~\neven more\n~~~";
589 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
590 let result = rule.check(&ctx).unwrap();
591
592 assert_eq!(result.len(), 2);
594 assert_eq!(result[0].line, 5);
595 assert_eq!(result[1].line, 7);
596 }
597
598 #[test]
599 fn test_detect_style_backtick() {
600 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
601 let ctx = LintContext::new("```\ncode\n```", crate::config::MarkdownFlavor::Standard, None);
602 let style = rule.detect_style(&ctx);
603
604 assert_eq!(style, Some(CodeFenceStyle::Backtick));
605 }
606
607 #[test]
608 fn test_detect_style_tilde() {
609 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
610 let ctx = LintContext::new("~~~\ncode\n~~~", crate::config::MarkdownFlavor::Standard, None);
611 let style = rule.detect_style(&ctx);
612
613 assert_eq!(style, Some(CodeFenceStyle::Tilde));
614 }
615
616 #[test]
617 fn test_detect_style_none() {
618 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
619 let ctx = LintContext::new("No code fences here", crate::config::MarkdownFlavor::Standard, None);
620 let style = rule.detect_style(&ctx);
621
622 assert_eq!(style, None);
623 }
624
625 #[test]
626 fn test_fix_backticks_to_tildes() {
627 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
628 let content = "```\ncode\n```";
629 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
630 let fixed = rule.fix(&ctx).unwrap();
631
632 assert_eq!(fixed, "~~~\ncode\n~~~");
633 }
634
635 #[test]
636 fn test_fix_tildes_to_backticks() {
637 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
638 let content = "~~~\ncode\n~~~";
639 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
640 let fixed = rule.fix(&ctx).unwrap();
641
642 assert_eq!(fixed, "```\ncode\n```");
643 }
644
645 #[test]
646 fn test_fix_preserves_fence_length() {
647 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
648 let content = "````\ncode with backtick\n```\ncode\n````";
649 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
650 let fixed = rule.fix(&ctx).unwrap();
651
652 assert_eq!(fixed, "~~~~\ncode with backtick\n```\ncode\n~~~~");
653 }
654
655 #[test]
656 fn test_fix_preserves_language_info() {
657 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
658 let content = "~~~rust\nfn main() {}\n~~~";
659 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
660 let fixed = rule.fix(&ctx).unwrap();
661
662 assert_eq!(fixed, "```rust\nfn main() {}\n```");
663 }
664
665 #[test]
666 fn test_indented_code_fences() {
667 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
668 let content = " ```\n code\n ```";
669 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
670 let result = rule.check(&ctx).unwrap();
671
672 assert_eq!(result.len(), 2);
673 }
674
675 #[test]
676 fn test_fix_indented_fences() {
677 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
678 let content = " ```\n code\n ```";
679 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
680 let fixed = rule.fix(&ctx).unwrap();
681
682 assert_eq!(fixed, " ~~~\n code\n ~~~");
683 }
684
685 #[test]
686 fn test_nested_fences_not_changed() {
687 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
688 let content = "```\ncode with ``` inside\n```";
689 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
690 let fixed = rule.fix(&ctx).unwrap();
691
692 assert_eq!(fixed, "~~~\ncode with ``` inside\n~~~");
693 }
694
695 #[test]
696 fn test_multiple_code_blocks() {
697 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
698 let content = "~~~\ncode1\n~~~\n\nText\n\n~~~python\ncode2\n~~~";
699 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
700 let result = rule.check(&ctx).unwrap();
701
702 assert_eq!(result.len(), 4); }
704
705 #[test]
706 fn test_empty_content() {
707 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
708 let content = "";
709 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
710 let result = rule.check(&ctx).unwrap();
711
712 assert_eq!(result.len(), 0);
713 }
714
715 #[test]
716 fn test_preserve_trailing_newline() {
717 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
718 let content = "~~~\ncode\n~~~\n";
719 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
720 let fixed = rule.fix(&ctx).unwrap();
721
722 assert_eq!(fixed, "```\ncode\n```\n");
723 }
724
725 #[test]
726 fn test_no_trailing_newline() {
727 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
728 let content = "~~~\ncode\n~~~";
729 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
730 let fixed = rule.fix(&ctx).unwrap();
731
732 assert_eq!(fixed, "```\ncode\n```");
733 }
734
735 #[test]
736 fn test_default_config() {
737 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
738 let (name, _config) = rule.default_config_section().unwrap();
739 assert_eq!(name, "MD048");
740 }
741
742 #[test]
745 fn test_tilde_outer_with_backtick_inner_uses_longer_fence() {
746 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
747 let content = "~~~text\n```rust\ncode\n```\n~~~";
748 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
749 let fixed = rule.fix(&ctx).unwrap();
750
751 assert_eq!(fixed, "````text\n```rust\ncode\n```\n````");
753 }
754
755 #[test]
758 fn test_check_tilde_outer_with_backtick_inner_warns_with_correct_replacement() {
759 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
760 let content = "~~~text\n```rust\ncode\n```\n~~~";
761 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
762 let warnings = rule.check(&ctx).unwrap();
763
764 assert_eq!(warnings.len(), 2);
766 let open_fix = warnings[0].fix.as_ref().unwrap();
767 let close_fix = warnings[1].fix.as_ref().unwrap();
768 assert_eq!(open_fix.replacement, "````text");
769 assert_eq!(close_fix.replacement, "````");
770 }
771
772 #[test]
775 fn test_tilde_outer_with_longer_backtick_inner() {
776 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
777 let content = "~~~text\n````rust\ncode\n````\n~~~";
778 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
779 let fixed = rule.fix(&ctx).unwrap();
780
781 assert_eq!(fixed, "`````text\n````rust\ncode\n````\n`````");
782 }
783
784 #[test]
787 fn test_backtick_outer_with_tilde_inner_uses_longer_fence() {
788 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
789 let content = "```text\n~~~rust\ncode\n~~~\n```";
790 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
791 let fixed = rule.fix(&ctx).unwrap();
792
793 assert_eq!(fixed, "~~~~text\n~~~rust\ncode\n~~~\n~~~~");
794 }
795
796 #[test]
804 fn test_info_string_interior_not_ambiguous() {
805 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
806 let content = "```text\n```rust\ncode\n```\n```";
812 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
813 let warnings = rule.check(&ctx).unwrap();
814
815 assert_eq!(warnings.len(), 0, "expected 0 warnings, got {warnings:?}");
818 }
819
820 #[test]
822 fn test_info_string_interior_fix_unchanged() {
823 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
824 let content = "```text\n```rust\ncode\n```\n```";
825 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
826 let fixed = rule.fix(&ctx).unwrap();
827
828 assert_eq!(fixed, content);
830 }
831
832 #[test]
834 fn test_tilde_info_string_interior_not_ambiguous() {
835 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
836 let content = "~~~text\n~~~rust\ncode\n~~~\n~~~";
837 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
838 let fixed = rule.fix(&ctx).unwrap();
839
840 assert_eq!(fixed, content);
842 }
843
844 #[test]
846 fn test_no_ambiguity_when_outer_is_longer() {
847 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
848 let content = "````text\n```rust\ncode\n```\n````";
849 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
850 let warnings = rule.check(&ctx).unwrap();
851
852 assert_eq!(
853 warnings.len(),
854 0,
855 "should have no warnings when outer is already longer"
856 );
857 }
858
859 #[test]
863 fn test_longer_info_string_interior_not_ambiguous() {
864 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
865 let content = "```text\n`````rust\ncode\n`````\n```";
871 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
872 let fixed = rule.fix(&ctx).unwrap();
873
874 assert_eq!(fixed, content);
876 }
877
878 #[test]
880 fn test_info_string_interior_consistent_style_no_warning() {
881 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
882 let content = "```text\n```rust\ncode\n```\n```";
883 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
884 let warnings = rule.check(&ctx).unwrap();
885
886 assert_eq!(warnings.len(), 0);
887 }
888
889 #[test]
896 fn test_cross_style_bare_inner_requires_lengthening() {
897 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
898 let content = "~~~\n`````rust\ncode\n```\n~~~";
902 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
903 let fixed = rule.fix(&ctx).unwrap();
904
905 assert_eq!(fixed, "````\n`````rust\ncode\n```\n````");
908 }
909
910 #[test]
914 fn test_cross_style_info_only_interior_no_lengthening() {
915 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
916 let content = "~~~text\n```rust\nexample\n```rust\n~~~";
920 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
921 let fixed = rule.fix(&ctx).unwrap();
922
923 assert_eq!(fixed, "```text\n```rust\nexample\n```rust\n```");
924 }
925
926 #[test]
929 fn test_same_style_info_outer_shorter_bare_interior_no_warning() {
930 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
931 let content = "````text\n```\nshowing raw fence\n```\n````";
935 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
936 let warnings = rule.check(&ctx).unwrap();
937
938 assert_eq!(
939 warnings.len(),
940 0,
941 "shorter bare interior sequences cannot close a 4-backtick outer"
942 );
943 }
944
945 #[test]
948 fn test_same_style_no_info_outer_shorter_bare_interior_no_warning() {
949 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
950 let content = "````\n```\nsome code\n```\n````";
953 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
954 let warnings = rule.check(&ctx).unwrap();
955
956 assert_eq!(
957 warnings.len(),
958 0,
959 "shorter bare interior sequences cannot close a 4-backtick outer (no info)"
960 );
961 }
962
963 #[test]
966 fn test_overindented_inner_sequence_not_ambiguous() {
967 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
968 let content = "```text\n ```\ncode\n```";
969 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
970 let warnings = rule.check(&ctx).unwrap();
971 let fixed = rule.fix(&ctx).unwrap();
972
973 assert_eq!(warnings.len(), 0, "over-indented inner fence should not warn");
974 assert_eq!(fixed, content, "over-indented inner fence should remain unchanged");
975 }
976
977 #[test]
980 fn test_conversion_ignores_overindented_inner_sequence_for_closing_detection() {
981 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
982 let content = "~~~text\n ~~~\n```rust\ncode\n```\n~~~";
983 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
984 let fixed = rule.fix(&ctx).unwrap();
985
986 assert_eq!(fixed, "````text\n ~~~\n```rust\ncode\n```\n````");
987 }
988
989 #[test]
992 fn test_top_level_four_space_fence_marker_is_ignored() {
993 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
994 let content = " ```\n code\n ```";
995 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
996 let warnings = rule.check(&ctx).unwrap();
997 let fixed = rule.fix(&ctx).unwrap();
998
999 assert_eq!(warnings.len(), 0);
1000 assert_eq!(fixed, content);
1001 }
1002
1003 #[test]
1007 fn test_fix_idempotent_no_double_blanks_with_nested_fences() {
1008 use crate::fix_coordinator::FixCoordinator;
1009 use crate::rules::Rule;
1010 use crate::rules::md013_line_length::MD013LineLength;
1011
1012 let content = "\
1016- **edition**: Rust edition to use by default for the code snippets. Default is `\"2015\"`. \
1017Individual code blocks can be controlled with the `edition2015`, `edition2018`, `edition2021` \
1018or `edition2024` annotations, such as:
1019
1020 ~~~text
1021 ```rust,edition2015
1022 // This only works in 2015.
1023 let try = true;
1024 ```
1025 ~~~
1026
1027### Build options
1028";
1029 let rules: Vec<Box<dyn Rule>> = vec![
1030 Box::new(MD013LineLength::new(80, false, false, false, true)),
1031 Box::new(MD048CodeFenceStyle::new(CodeFenceStyle::Backtick)),
1032 ];
1033
1034 let mut first_pass = content.to_string();
1035 let coordinator = FixCoordinator::new();
1036 coordinator
1037 .apply_fixes_iterative(&rules, &[], &mut first_pass, &Default::default(), 10, None)
1038 .expect("fix should not fail");
1039
1040 let lines: Vec<&str> = first_pass.lines().collect();
1042 for i in 0..lines.len().saturating_sub(1) {
1043 assert!(
1044 !(lines[i].is_empty() && lines[i + 1].is_empty()),
1045 "Double blank at lines {},{} after first pass:\n{first_pass}",
1046 i + 1,
1047 i + 2
1048 );
1049 }
1050
1051 let mut second_pass = first_pass.clone();
1053 let rules2: Vec<Box<dyn Rule>> = vec![
1054 Box::new(MD013LineLength::new(80, false, false, false, true)),
1055 Box::new(MD048CodeFenceStyle::new(CodeFenceStyle::Backtick)),
1056 ];
1057 let coordinator2 = FixCoordinator::new();
1058 coordinator2
1059 .apply_fixes_iterative(&rules2, &[], &mut second_pass, &Default::default(), 10, None)
1060 .expect("fix should not fail");
1061
1062 assert_eq!(
1063 first_pass, second_pass,
1064 "Fix is not idempotent:\nFirst pass:\n{first_pass}\nSecond pass:\n{second_pass}"
1065 );
1066 }
1067}