1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, 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 category(&self) -> RuleCategory {
182 RuleCategory::CodeBlock
183 }
184
185 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
186 let content = ctx.content;
187 let line_index = &ctx.line_index;
188
189 let mut warnings = Vec::new();
190
191 let target_style = match self.config.style {
192 CodeFenceStyle::Consistent => self.detect_style(ctx).unwrap_or(CodeFenceStyle::Backtick),
193 _ => self.config.style,
194 };
195
196 let lines: Vec<&str> = content.lines().collect();
197 let mut in_code_block = false;
198 let mut code_block_fence_char = '`';
199 let mut code_block_fence_len = 0usize;
200 let mut converted_fence_len = 0usize;
203 let mut needs_lengthening = false;
206
207 for (line_num, &line) in lines.iter().enumerate() {
208 let Some(marker) = parse_fence_marker(line) else {
209 continue;
210 };
211 let fence_char = marker.fence_char;
212 let fence_len = marker.fence_len;
213
214 if !in_code_block {
215 in_code_block = true;
216 code_block_fence_char = fence_char;
217 code_block_fence_len = fence_len;
218
219 let needs_conversion = (fence_char == '`' && target_style == CodeFenceStyle::Tilde)
220 || (fence_char == '~' && target_style == CodeFenceStyle::Backtick);
221
222 if needs_conversion {
223 let target_char = if target_style == CodeFenceStyle::Backtick {
224 '`'
225 } else {
226 '~'
227 };
228
229 let prefix = &line[..marker.fence_start];
232 let info = marker.rest;
233 let max_inner =
234 max_inner_fence_length_of_char(&lines, line_num, fence_len, fence_char, target_char);
235 converted_fence_len = fence_len.max(max_inner + 1);
236 needs_lengthening = false;
237
238 let replacement = format!("{prefix}{}{info}", target_char.to_string().repeat(converted_fence_len));
239
240 let fence_start = marker.fence_start;
241 let fence_end = fence_start + fence_len;
242 let (start_line, start_col, end_line, end_col) =
243 calculate_match_range(line_num + 1, line, fence_start, fence_end - fence_start);
244
245 warnings.push(LintWarning {
246 rule_name: Some(self.name().to_string()),
247 message: format!(
248 "Code fence style: use {} instead of {}",
249 if target_style == CodeFenceStyle::Backtick {
250 "```"
251 } else {
252 "~~~"
253 },
254 if fence_char == '`' { "```" } else { "~~~" }
255 ),
256 line: start_line,
257 column: start_col,
258 end_line,
259 end_column: end_col,
260 severity: Severity::Warning,
261 fix: Some(Fix {
262 range: line_index.line_col_to_byte_range_with_length(line_num + 1, 1, line.len()),
263 replacement,
264 }),
265 });
266 } else {
267 let prefix = &line[..marker.fence_start];
272 let info = marker.rest;
273 let max_inner = max_inner_fence_length_of_char(&lines, line_num, fence_len, fence_char, fence_char);
274 if max_inner >= fence_len {
275 converted_fence_len = max_inner + 1;
276 needs_lengthening = true;
277
278 let replacement =
279 format!("{prefix}{}{info}", fence_char.to_string().repeat(converted_fence_len));
280
281 let fence_start = marker.fence_start;
282 let fence_end = fence_start + fence_len;
283 let (start_line, start_col, end_line, end_col) =
284 calculate_match_range(line_num + 1, line, fence_start, fence_end - fence_start);
285
286 warnings.push(LintWarning {
287 rule_name: Some(self.name().to_string()),
288 message: format!(
289 "Code fence length is ambiguous: outer fence ({fence_len} {}) \
290 contains interior fence sequences of equal length; \
291 use {converted_fence_len}",
292 if fence_char == '`' { "backticks" } else { "tildes" },
293 ),
294 line: start_line,
295 column: start_col,
296 end_line,
297 end_column: end_col,
298 severity: Severity::Warning,
299 fix: Some(Fix {
300 range: line_index.line_col_to_byte_range_with_length(line_num + 1, 1, line.len()),
301 replacement,
302 }),
303 });
304 } else {
305 converted_fence_len = fence_len;
306 needs_lengthening = false;
307 }
308 }
309 } else {
310 let is_closing = is_closing_fence(marker, code_block_fence_char, code_block_fence_len);
312
313 if is_closing {
314 let needs_conversion = (fence_char == '`' && target_style == CodeFenceStyle::Tilde)
315 || (fence_char == '~' && target_style == CodeFenceStyle::Backtick);
316
317 if needs_conversion || needs_lengthening {
318 let target_char = if needs_conversion {
319 if target_style == CodeFenceStyle::Backtick {
320 '`'
321 } else {
322 '~'
323 }
324 } else {
325 fence_char
326 };
327
328 let prefix = &line[..marker.fence_start];
329 let replacement = format!(
330 "{prefix}{}{}",
331 target_char.to_string().repeat(converted_fence_len),
332 marker.rest
333 );
334
335 let fence_start = marker.fence_start;
336 let fence_end = fence_start + fence_len;
337 let (start_line, start_col, end_line, end_col) =
338 calculate_match_range(line_num + 1, line, fence_start, fence_end - fence_start);
339
340 let message = if needs_conversion {
341 format!(
342 "Code fence style: use {} instead of {}",
343 if target_style == CodeFenceStyle::Backtick {
344 "```"
345 } else {
346 "~~~"
347 },
348 if fence_char == '`' { "```" } else { "~~~" }
349 )
350 } else {
351 format!(
352 "Code fence length is ambiguous: closing fence ({fence_len} {}) \
353 must match the lengthened outer fence; use {converted_fence_len}",
354 if fence_char == '`' { "backticks" } else { "tildes" },
355 )
356 };
357
358 warnings.push(LintWarning {
359 rule_name: Some(self.name().to_string()),
360 message,
361 line: start_line,
362 column: start_col,
363 end_line,
364 end_column: end_col,
365 severity: Severity::Warning,
366 fix: Some(Fix {
367 range: line_index.line_col_to_byte_range_with_length(line_num + 1, 1, line.len()),
368 replacement,
369 }),
370 });
371 }
372
373 in_code_block = false;
374 code_block_fence_len = 0;
375 converted_fence_len = 0;
376 needs_lengthening = false;
377 }
378 }
380 }
381
382 Ok(warnings)
383 }
384
385 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
387 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~'))
389 }
390
391 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
392 let content = ctx.content;
393
394 let target_style = match self.config.style {
395 CodeFenceStyle::Consistent => self.detect_style(ctx).unwrap_or(CodeFenceStyle::Backtick),
396 _ => self.config.style,
397 };
398
399 let lines: Vec<&str> = content.lines().collect();
400 let mut result = String::new();
401 let mut in_code_block = false;
402 let mut code_block_fence_char = '`';
403 let mut code_block_fence_len = 0usize;
404 let mut converted_fence_len = 0usize;
405 let mut needs_lengthening = false;
406
407 for (line_idx, &line) in lines.iter().enumerate() {
408 let line_num = line_idx + 1;
409
410 if ctx.inline_config().is_rule_disabled(self.name(), line_num) {
412 result.push_str(line);
413 if let Some(marker) = parse_fence_marker(line) {
415 if !in_code_block {
416 in_code_block = true;
417 code_block_fence_char = marker.fence_char;
418 code_block_fence_len = marker.fence_len;
419 converted_fence_len = marker.fence_len;
420 needs_lengthening = false;
421 } else if is_closing_fence(marker, code_block_fence_char, code_block_fence_len) {
422 in_code_block = false;
423 code_block_fence_len = 0;
424 converted_fence_len = 0;
425 needs_lengthening = false;
426 }
427 }
428 result.push('\n');
429 continue;
430 }
431
432 if let Some(marker) = parse_fence_marker(line) {
433 let fence_char = marker.fence_char;
434 let fence_len = marker.fence_len;
435
436 if !in_code_block {
437 in_code_block = true;
438 code_block_fence_char = fence_char;
439 code_block_fence_len = fence_len;
440
441 let needs_conversion = (fence_char == '`' && target_style == CodeFenceStyle::Tilde)
442 || (fence_char == '~' && target_style == CodeFenceStyle::Backtick);
443
444 let prefix = &line[..marker.fence_start];
445 let info = marker.rest;
446
447 if needs_conversion {
448 let target_char = if target_style == CodeFenceStyle::Backtick {
449 '`'
450 } else {
451 '~'
452 };
453
454 let max_inner =
455 max_inner_fence_length_of_char(&lines, line_idx, fence_len, fence_char, target_char);
456 converted_fence_len = fence_len.max(max_inner + 1);
457 needs_lengthening = false;
458
459 result.push_str(prefix);
460 result.push_str(&target_char.to_string().repeat(converted_fence_len));
461 result.push_str(info);
462 } else {
463 let max_inner =
465 max_inner_fence_length_of_char(&lines, line_idx, fence_len, fence_char, fence_char);
466 if max_inner >= fence_len {
467 converted_fence_len = max_inner + 1;
468 needs_lengthening = true;
469
470 result.push_str(prefix);
471 result.push_str(&fence_char.to_string().repeat(converted_fence_len));
472 result.push_str(info);
473 } else {
474 converted_fence_len = fence_len;
475 needs_lengthening = false;
476 result.push_str(line);
477 }
478 }
479 } else {
480 let is_closing = is_closing_fence(marker, code_block_fence_char, code_block_fence_len);
482
483 if is_closing {
484 let needs_conversion = (fence_char == '`' && target_style == CodeFenceStyle::Tilde)
485 || (fence_char == '~' && target_style == CodeFenceStyle::Backtick);
486
487 if needs_conversion || needs_lengthening {
488 let target_char = if needs_conversion {
489 if target_style == CodeFenceStyle::Backtick {
490 '`'
491 } else {
492 '~'
493 }
494 } else {
495 fence_char
496 };
497 let prefix = &line[..marker.fence_start];
498 result.push_str(prefix);
499 result.push_str(&target_char.to_string().repeat(converted_fence_len));
500 result.push_str(marker.rest);
501 } else {
502 result.push_str(line);
503 }
504
505 in_code_block = false;
506 code_block_fence_len = 0;
507 converted_fence_len = 0;
508 needs_lengthening = false;
509 } else {
510 result.push_str(line);
512 }
513 }
514 } else {
515 result.push_str(line);
516 }
517 result.push('\n');
518 }
519
520 if !content.ends_with('\n') && result.ends_with('\n') {
522 result.pop();
523 }
524
525 Ok(result)
526 }
527
528 fn as_any(&self) -> &dyn std::any::Any {
529 self
530 }
531
532 fn default_config_section(&self) -> Option<(String, toml::Value)> {
533 let json_value = serde_json::to_value(&self.config).ok()?;
534 Some((
535 self.name().to_string(),
536 crate::rule_config_serde::json_to_toml_value(&json_value)?,
537 ))
538 }
539
540 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
541 where
542 Self: Sized,
543 {
544 let rule_config = crate::rule_config_serde::load_rule_config::<MD048Config>(config);
545 Box::new(Self::from_config_struct(rule_config))
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use crate::lint_context::LintContext;
553
554 #[test]
555 fn test_backtick_style_with_backticks() {
556 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
557 let content = "```\ncode\n```";
558 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
559 let result = rule.check(&ctx).unwrap();
560
561 assert_eq!(result.len(), 0);
562 }
563
564 #[test]
565 fn test_backtick_style_with_tildes() {
566 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
567 let content = "~~~\ncode\n~~~";
568 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
569 let result = rule.check(&ctx).unwrap();
570
571 assert_eq!(result.len(), 2); assert!(result[0].message.contains("use ``` instead of ~~~"));
573 assert_eq!(result[0].line, 1);
574 assert_eq!(result[1].line, 3);
575 }
576
577 #[test]
578 fn test_tilde_style_with_tildes() {
579 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
580 let content = "~~~\ncode\n~~~";
581 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
582 let result = rule.check(&ctx).unwrap();
583
584 assert_eq!(result.len(), 0);
585 }
586
587 #[test]
588 fn test_tilde_style_with_backticks() {
589 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
590 let content = "```\ncode\n```";
591 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
592 let result = rule.check(&ctx).unwrap();
593
594 assert_eq!(result.len(), 2); assert!(result[0].message.contains("use ~~~ instead of ```"));
596 }
597
598 #[test]
599 fn test_consistent_style_tie_prefers_backtick() {
600 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
601 let content = "```\ncode\n```\n\n~~~\nmore code\n~~~";
603 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
604 let result = rule.check(&ctx).unwrap();
605
606 assert_eq!(result.len(), 2);
608 assert_eq!(result[0].line, 5);
609 assert_eq!(result[1].line, 7);
610 }
611
612 #[test]
613 fn test_consistent_style_tilde_most_prevalent() {
614 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
615 let content = "~~~\ncode\n~~~\n\n```\nmore code\n```\n\n~~~\neven more\n~~~";
617 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
618 let result = rule.check(&ctx).unwrap();
619
620 assert_eq!(result.len(), 2);
622 assert_eq!(result[0].line, 5);
623 assert_eq!(result[1].line, 7);
624 }
625
626 #[test]
627 fn test_detect_style_backtick() {
628 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
629 let ctx = LintContext::new("```\ncode\n```", crate::config::MarkdownFlavor::Standard, None);
630 let style = rule.detect_style(&ctx);
631
632 assert_eq!(style, Some(CodeFenceStyle::Backtick));
633 }
634
635 #[test]
636 fn test_detect_style_tilde() {
637 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
638 let ctx = LintContext::new("~~~\ncode\n~~~", crate::config::MarkdownFlavor::Standard, None);
639 let style = rule.detect_style(&ctx);
640
641 assert_eq!(style, Some(CodeFenceStyle::Tilde));
642 }
643
644 #[test]
645 fn test_detect_style_none() {
646 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
647 let ctx = LintContext::new("No code fences here", crate::config::MarkdownFlavor::Standard, None);
648 let style = rule.detect_style(&ctx);
649
650 assert_eq!(style, None);
651 }
652
653 #[test]
654 fn test_fix_backticks_to_tildes() {
655 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
656 let content = "```\ncode\n```";
657 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
658 let fixed = rule.fix(&ctx).unwrap();
659
660 assert_eq!(fixed, "~~~\ncode\n~~~");
661 }
662
663 #[test]
664 fn test_fix_tildes_to_backticks() {
665 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
666 let content = "~~~\ncode\n~~~";
667 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
668 let fixed = rule.fix(&ctx).unwrap();
669
670 assert_eq!(fixed, "```\ncode\n```");
671 }
672
673 #[test]
674 fn test_fix_preserves_fence_length() {
675 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
676 let content = "````\ncode with backtick\n```\ncode\n````";
677 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
678 let fixed = rule.fix(&ctx).unwrap();
679
680 assert_eq!(fixed, "~~~~\ncode with backtick\n```\ncode\n~~~~");
681 }
682
683 #[test]
684 fn test_fix_preserves_language_info() {
685 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
686 let content = "~~~rust\nfn main() {}\n~~~";
687 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
688 let fixed = rule.fix(&ctx).unwrap();
689
690 assert_eq!(fixed, "```rust\nfn main() {}\n```");
691 }
692
693 #[test]
694 fn test_indented_code_fences() {
695 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
696 let content = " ```\n code\n ```";
697 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
698 let result = rule.check(&ctx).unwrap();
699
700 assert_eq!(result.len(), 2);
701 }
702
703 #[test]
704 fn test_fix_indented_fences() {
705 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
706 let content = " ```\n code\n ```";
707 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
708 let fixed = rule.fix(&ctx).unwrap();
709
710 assert_eq!(fixed, " ~~~\n code\n ~~~");
711 }
712
713 #[test]
714 fn test_nested_fences_not_changed() {
715 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
716 let content = "```\ncode with ``` inside\n```";
717 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
718 let fixed = rule.fix(&ctx).unwrap();
719
720 assert_eq!(fixed, "~~~\ncode with ``` inside\n~~~");
721 }
722
723 #[test]
724 fn test_multiple_code_blocks() {
725 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
726 let content = "~~~\ncode1\n~~~\n\nText\n\n~~~python\ncode2\n~~~";
727 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
728 let result = rule.check(&ctx).unwrap();
729
730 assert_eq!(result.len(), 4); }
732
733 #[test]
734 fn test_empty_content() {
735 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
736 let content = "";
737 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
738 let result = rule.check(&ctx).unwrap();
739
740 assert_eq!(result.len(), 0);
741 }
742
743 #[test]
744 fn test_preserve_trailing_newline() {
745 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
746 let content = "~~~\ncode\n~~~\n";
747 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
748 let fixed = rule.fix(&ctx).unwrap();
749
750 assert_eq!(fixed, "```\ncode\n```\n");
751 }
752
753 #[test]
754 fn test_no_trailing_newline() {
755 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
756 let content = "~~~\ncode\n~~~";
757 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
758 let fixed = rule.fix(&ctx).unwrap();
759
760 assert_eq!(fixed, "```\ncode\n```");
761 }
762
763 #[test]
764 fn test_default_config() {
765 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
766 let (name, _config) = rule.default_config_section().unwrap();
767 assert_eq!(name, "MD048");
768 }
769
770 #[test]
773 fn test_tilde_outer_with_backtick_inner_uses_longer_fence() {
774 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
775 let content = "~~~text\n```rust\ncode\n```\n~~~";
776 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
777 let fixed = rule.fix(&ctx).unwrap();
778
779 assert_eq!(fixed, "````text\n```rust\ncode\n```\n````");
781 }
782
783 #[test]
786 fn test_check_tilde_outer_with_backtick_inner_warns_with_correct_replacement() {
787 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
788 let content = "~~~text\n```rust\ncode\n```\n~~~";
789 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
790 let warnings = rule.check(&ctx).unwrap();
791
792 assert_eq!(warnings.len(), 2);
794 let open_fix = warnings[0].fix.as_ref().unwrap();
795 let close_fix = warnings[1].fix.as_ref().unwrap();
796 assert_eq!(open_fix.replacement, "````text");
797 assert_eq!(close_fix.replacement, "````");
798 }
799
800 #[test]
803 fn test_tilde_outer_with_longer_backtick_inner() {
804 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
805 let content = "~~~text\n````rust\ncode\n````\n~~~";
806 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
807 let fixed = rule.fix(&ctx).unwrap();
808
809 assert_eq!(fixed, "`````text\n````rust\ncode\n````\n`````");
810 }
811
812 #[test]
815 fn test_backtick_outer_with_tilde_inner_uses_longer_fence() {
816 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
817 let content = "```text\n~~~rust\ncode\n~~~\n```";
818 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
819 let fixed = rule.fix(&ctx).unwrap();
820
821 assert_eq!(fixed, "~~~~text\n~~~rust\ncode\n~~~\n~~~~");
822 }
823
824 #[test]
832 fn test_info_string_interior_not_ambiguous() {
833 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
834 let content = "```text\n```rust\ncode\n```\n```";
840 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
841 let warnings = rule.check(&ctx).unwrap();
842
843 assert_eq!(warnings.len(), 0, "expected 0 warnings, got {warnings:?}");
846 }
847
848 #[test]
850 fn test_info_string_interior_fix_unchanged() {
851 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
852 let content = "```text\n```rust\ncode\n```\n```";
853 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
854 let fixed = rule.fix(&ctx).unwrap();
855
856 assert_eq!(fixed, content);
858 }
859
860 #[test]
862 fn test_tilde_info_string_interior_not_ambiguous() {
863 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
864 let content = "~~~text\n~~~rust\ncode\n~~~\n~~~";
865 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
866 let fixed = rule.fix(&ctx).unwrap();
867
868 assert_eq!(fixed, content);
870 }
871
872 #[test]
874 fn test_no_ambiguity_when_outer_is_longer() {
875 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
876 let content = "````text\n```rust\ncode\n```\n````";
877 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
878 let warnings = rule.check(&ctx).unwrap();
879
880 assert_eq!(
881 warnings.len(),
882 0,
883 "should have no warnings when outer is already longer"
884 );
885 }
886
887 #[test]
891 fn test_longer_info_string_interior_not_ambiguous() {
892 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
893 let content = "```text\n`````rust\ncode\n`````\n```";
899 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
900 let fixed = rule.fix(&ctx).unwrap();
901
902 assert_eq!(fixed, content);
904 }
905
906 #[test]
908 fn test_info_string_interior_consistent_style_no_warning() {
909 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
910 let content = "```text\n```rust\ncode\n```\n```";
911 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
912 let warnings = rule.check(&ctx).unwrap();
913
914 assert_eq!(warnings.len(), 0);
915 }
916
917 #[test]
924 fn test_cross_style_bare_inner_requires_lengthening() {
925 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
926 let content = "~~~\n`````rust\ncode\n```\n~~~";
930 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
931 let fixed = rule.fix(&ctx).unwrap();
932
933 assert_eq!(fixed, "````\n`````rust\ncode\n```\n````");
936 }
937
938 #[test]
942 fn test_cross_style_info_only_interior_no_lengthening() {
943 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
944 let content = "~~~text\n```rust\nexample\n```rust\n~~~";
948 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
949 let fixed = rule.fix(&ctx).unwrap();
950
951 assert_eq!(fixed, "```text\n```rust\nexample\n```rust\n```");
952 }
953
954 #[test]
957 fn test_same_style_info_outer_shorter_bare_interior_no_warning() {
958 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
959 let content = "````text\n```\nshowing raw fence\n```\n````";
963 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
964 let warnings = rule.check(&ctx).unwrap();
965
966 assert_eq!(
967 warnings.len(),
968 0,
969 "shorter bare interior sequences cannot close a 4-backtick outer"
970 );
971 }
972
973 #[test]
976 fn test_same_style_no_info_outer_shorter_bare_interior_no_warning() {
977 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
978 let content = "````\n```\nsome code\n```\n````";
981 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
982 let warnings = rule.check(&ctx).unwrap();
983
984 assert_eq!(
985 warnings.len(),
986 0,
987 "shorter bare interior sequences cannot close a 4-backtick outer (no info)"
988 );
989 }
990
991 #[test]
994 fn test_overindented_inner_sequence_not_ambiguous() {
995 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
996 let content = "```text\n ```\ncode\n```";
997 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
998 let warnings = rule.check(&ctx).unwrap();
999 let fixed = rule.fix(&ctx).unwrap();
1000
1001 assert_eq!(warnings.len(), 0, "over-indented inner fence should not warn");
1002 assert_eq!(fixed, content, "over-indented inner fence should remain unchanged");
1003 }
1004
1005 #[test]
1008 fn test_conversion_ignores_overindented_inner_sequence_for_closing_detection() {
1009 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
1010 let content = "~~~text\n ~~~\n```rust\ncode\n```\n~~~";
1011 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1012 let fixed = rule.fix(&ctx).unwrap();
1013
1014 assert_eq!(fixed, "````text\n ~~~\n```rust\ncode\n```\n````");
1015 }
1016
1017 #[test]
1020 fn test_top_level_four_space_fence_marker_is_ignored() {
1021 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
1022 let content = " ```\n code\n ```";
1023 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1024 let warnings = rule.check(&ctx).unwrap();
1025 let fixed = rule.fix(&ctx).unwrap();
1026
1027 assert_eq!(warnings.len(), 0);
1028 assert_eq!(fixed, content);
1029 }
1030
1031 #[test]
1035 fn test_fix_idempotent_no_double_blanks_with_nested_fences() {
1036 use crate::fix_coordinator::FixCoordinator;
1037 use crate::rules::Rule;
1038 use crate::rules::md013_line_length::MD013LineLength;
1039
1040 let content = "\
1044- **edition**: Rust edition to use by default for the code snippets. Default is `\"2015\"`. \
1045Individual code blocks can be controlled with the `edition2015`, `edition2018`, `edition2021` \
1046or `edition2024` annotations, such as:
1047
1048 ~~~text
1049 ```rust,edition2015
1050 // This only works in 2015.
1051 let try = true;
1052 ```
1053 ~~~
1054
1055### Build options
1056";
1057 let rules: Vec<Box<dyn Rule>> = vec![
1058 Box::new(MD013LineLength::new(80, false, false, false, true)),
1059 Box::new(MD048CodeFenceStyle::new(CodeFenceStyle::Backtick)),
1060 ];
1061
1062 let mut first_pass = content.to_string();
1063 let coordinator = FixCoordinator::new();
1064 coordinator
1065 .apply_fixes_iterative(&rules, &[], &mut first_pass, &Default::default(), 10, None)
1066 .expect("fix should not fail");
1067
1068 let lines: Vec<&str> = first_pass.lines().collect();
1070 for i in 0..lines.len().saturating_sub(1) {
1071 assert!(
1072 !(lines[i].is_empty() && lines[i + 1].is_empty()),
1073 "Double blank at lines {},{} after first pass:\n{first_pass}",
1074 i + 1,
1075 i + 2
1076 );
1077 }
1078
1079 let mut second_pass = first_pass.clone();
1081 let rules2: Vec<Box<dyn Rule>> = vec![
1082 Box::new(MD013LineLength::new(80, false, false, false, true)),
1083 Box::new(MD048CodeFenceStyle::new(CodeFenceStyle::Backtick)),
1084 ];
1085 let coordinator2 = FixCoordinator::new();
1086 coordinator2
1087 .apply_fixes_iterative(&rules2, &[], &mut second_pass, &Default::default(), 10, None)
1088 .expect("fix should not fail");
1089
1090 assert_eq!(
1091 first_pass, second_pass,
1092 "Fix is not idempotent:\nFirst pass:\n{first_pass}\nSecond pass:\n{second_pass}"
1093 );
1094 }
1095}