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 (i, line) in ctx.content.lines().enumerate() {
95 if ctx.flavor.supports_colon_code_fences() && ctx.lines.get(i).is_some_and(|li| li.in_code_block) {
98 continue;
99 }
100
101 if ctx.flavor.supports_myst_directives() && ctx.lines.get(i).is_some_and(|li| li.in_myst_directive) {
104 continue;
105 }
106
107 let Some(marker) = parse_fence_marker(line) else {
108 continue;
109 };
110
111 if ctx.flavor.supports_myst_directives()
113 && marker.fence_char == '`'
114 && marker.rest.trim_start().starts_with('{')
115 {
116 continue;
117 }
118
119 if !in_code_block {
120 if marker.fence_char == '`' {
122 backtick_count += 1;
123 } else {
124 tilde_count += 1;
125 }
126 in_code_block = true;
127 opening_fence_char = marker.fence_char;
128 opening_fence_len = marker.fence_len;
129 } else if is_closing_fence(marker, opening_fence_char, opening_fence_len) {
130 in_code_block = false;
131 }
132 }
133
134 if backtick_count >= tilde_count && backtick_count > 0 {
137 Some(CodeFenceStyle::Backtick)
138 } else if tilde_count > 0 {
139 Some(CodeFenceStyle::Tilde)
140 } else {
141 None
142 }
143 }
144}
145
146fn max_inner_fence_length_of_char(
164 lines: &[&str],
165 opening_line: usize,
166 opening_fence_len: usize,
167 opening_char: char,
168 target_char: char,
169) -> usize {
170 let mut max_len = 0usize;
171
172 for line in lines.iter().skip(opening_line + 1) {
173 let Some(marker) = parse_fence_marker(line) else {
174 continue;
175 };
176
177 if is_closing_fence(marker, opening_char, opening_fence_len) {
179 break;
180 }
181
182 if marker.fence_char == target_char && marker.rest.trim().is_empty() {
185 max_len = max_len.max(marker.fence_len);
186 }
187 }
188
189 max_len
190}
191
192impl Rule for MD048CodeFenceStyle {
193 fn name(&self) -> &'static str {
194 "MD048"
195 }
196
197 fn description(&self) -> &'static str {
198 "Code fence style should be consistent"
199 }
200
201 fn category(&self) -> RuleCategory {
202 RuleCategory::CodeBlock
203 }
204
205 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
206 let content = ctx.content;
207 let line_index = &ctx.line_index;
208
209 let mut warnings = Vec::new();
210
211 let target_style = match self.config.style {
212 CodeFenceStyle::Consistent => self.detect_style(ctx).unwrap_or(CodeFenceStyle::Backtick),
213 _ => self.config.style,
214 };
215
216 let lines: Vec<&str> = content.lines().collect();
217 let mut in_code_block = false;
218 let mut code_block_fence_char = '`';
219 let mut code_block_fence_len = 0usize;
220 let mut converted_fence_len = 0usize;
223 let mut needs_lengthening = false;
226
227 for (line_num, &line) in lines.iter().enumerate() {
228 if ctx.flavor.supports_colon_code_fences() && ctx.lines.get(line_num).is_some_and(|li| li.in_code_block) {
230 continue;
231 }
232
233 if ctx.flavor.supports_myst_directives() && ctx.lines.get(line_num).is_some_and(|li| li.in_myst_directive) {
235 continue;
236 }
237
238 let Some(marker) = parse_fence_marker(line) else {
239 continue;
240 };
241
242 if ctx.flavor.supports_myst_directives()
244 && !in_code_block
245 && marker.fence_char == '`'
246 && marker.rest.trim_start().starts_with('{')
247 {
248 continue;
249 }
250 let fence_char = marker.fence_char;
251 let fence_len = marker.fence_len;
252
253 if !in_code_block {
254 in_code_block = true;
255 code_block_fence_char = fence_char;
256 code_block_fence_len = fence_len;
257
258 let needs_conversion = (fence_char == '`' && target_style == CodeFenceStyle::Tilde)
259 || (fence_char == '~' && target_style == CodeFenceStyle::Backtick);
260
261 if needs_conversion {
262 let target_char = if target_style == CodeFenceStyle::Backtick {
263 '`'
264 } else {
265 '~'
266 };
267
268 let prefix = &line[..marker.fence_start];
271 let info = marker.rest;
272 let max_inner =
273 max_inner_fence_length_of_char(&lines, line_num, fence_len, fence_char, target_char);
274 converted_fence_len = fence_len.max(max_inner + 1);
275 needs_lengthening = false;
276
277 let replacement = format!("{prefix}{}{info}", target_char.to_string().repeat(converted_fence_len));
278
279 let fence_start = marker.fence_start;
280 let fence_end = fence_start + fence_len;
281 let (start_line, start_col, end_line, end_col) =
282 calculate_match_range(line_num + 1, line, fence_start, fence_end - fence_start);
283
284 warnings.push(LintWarning {
285 rule_name: Some(self.name().to_string()),
286 message: format!(
287 "Code fence style: use {} instead of {}",
288 if target_style == CodeFenceStyle::Backtick {
289 "```"
290 } else {
291 "~~~"
292 },
293 if fence_char == '`' { "```" } else { "~~~" }
294 ),
295 line: start_line,
296 column: start_col,
297 end_line,
298 end_column: end_col,
299 severity: Severity::Warning,
300 fix: Some(Fix::new(
301 line_index.line_col_to_byte_range_with_length(line_num + 1, 1, line.len()),
302 replacement,
303 )),
304 });
305 } else {
306 let prefix = &line[..marker.fence_start];
311 let info = marker.rest;
312 let max_inner = max_inner_fence_length_of_char(&lines, line_num, fence_len, fence_char, fence_char);
313 if max_inner >= fence_len {
314 converted_fence_len = max_inner + 1;
315 needs_lengthening = true;
316
317 let replacement =
318 format!("{prefix}{}{info}", fence_char.to_string().repeat(converted_fence_len));
319
320 let fence_start = marker.fence_start;
321 let fence_end = fence_start + fence_len;
322 let (start_line, start_col, end_line, end_col) =
323 calculate_match_range(line_num + 1, line, fence_start, fence_end - fence_start);
324
325 warnings.push(LintWarning {
326 rule_name: Some(self.name().to_string()),
327 message: format!(
328 "Code fence length is ambiguous: outer fence ({fence_len} {}) \
329 contains interior fence sequences of equal length; \
330 use {converted_fence_len}",
331 if fence_char == '`' { "backticks" } else { "tildes" },
332 ),
333 line: start_line,
334 column: start_col,
335 end_line,
336 end_column: end_col,
337 severity: Severity::Warning,
338 fix: Some(Fix::new(
339 line_index.line_col_to_byte_range_with_length(line_num + 1, 1, line.len()),
340 replacement,
341 )),
342 });
343 } else {
344 converted_fence_len = fence_len;
345 needs_lengthening = false;
346 }
347 }
348 } else {
349 let is_closing = is_closing_fence(marker, code_block_fence_char, code_block_fence_len);
351
352 if is_closing {
353 let needs_conversion = (fence_char == '`' && target_style == CodeFenceStyle::Tilde)
354 || (fence_char == '~' && target_style == CodeFenceStyle::Backtick);
355
356 if needs_conversion || needs_lengthening {
357 let target_char = if needs_conversion {
358 if target_style == CodeFenceStyle::Backtick {
359 '`'
360 } else {
361 '~'
362 }
363 } else {
364 fence_char
365 };
366
367 let prefix = &line[..marker.fence_start];
368 let replacement = format!(
369 "{prefix}{}{}",
370 target_char.to_string().repeat(converted_fence_len),
371 marker.rest
372 );
373
374 let fence_start = marker.fence_start;
375 let fence_end = fence_start + fence_len;
376 let (start_line, start_col, end_line, end_col) =
377 calculate_match_range(line_num + 1, line, fence_start, fence_end - fence_start);
378
379 let message = if needs_conversion {
380 format!(
381 "Code fence style: use {} instead of {}",
382 if target_style == CodeFenceStyle::Backtick {
383 "```"
384 } else {
385 "~~~"
386 },
387 if fence_char == '`' { "```" } else { "~~~" }
388 )
389 } else {
390 format!(
391 "Code fence length is ambiguous: closing fence ({fence_len} {}) \
392 must match the lengthened outer fence; use {converted_fence_len}",
393 if fence_char == '`' { "backticks" } else { "tildes" },
394 )
395 };
396
397 warnings.push(LintWarning {
398 rule_name: Some(self.name().to_string()),
399 message,
400 line: start_line,
401 column: start_col,
402 end_line,
403 end_column: end_col,
404 severity: Severity::Warning,
405 fix: Some(Fix::new(
406 line_index.line_col_to_byte_range_with_length(line_num + 1, 1, line.len()),
407 replacement,
408 )),
409 });
410 }
411
412 in_code_block = false;
413 code_block_fence_len = 0;
414 converted_fence_len = 0;
415 needs_lengthening = false;
416 }
417 }
419 }
420
421 Ok(warnings)
422 }
423
424 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
426 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~'))
428 }
429
430 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
431 if self.should_skip(ctx) {
432 return Ok(ctx.content.to_string());
433 }
434 let warnings = self.check(ctx)?;
435 if warnings.is_empty() {
436 return Ok(ctx.content.to_string());
437 }
438 let warnings =
439 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
440 crate::utils::fix_utils::apply_warning_fixes(ctx.content, &warnings)
441 .map_err(crate::rule::LintError::InvalidInput)
442 }
443
444 fn as_any(&self) -> &dyn std::any::Any {
445 self
446 }
447
448 crate::impl_rule_config_methods!(MD048Config);
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use crate::lint_context::LintContext;
455
456 #[test]
457 fn test_backtick_style_with_backticks() {
458 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
459 let content = "```\ncode\n```";
460 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
461 let result = rule.check(&ctx).unwrap();
462
463 assert_eq!(result.len(), 0);
464 }
465
466 #[test]
467 fn test_backtick_style_with_tildes() {
468 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
469 let content = "~~~\ncode\n~~~";
470 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
471 let result = rule.check(&ctx).unwrap();
472
473 assert_eq!(result.len(), 2); assert!(result[0].message.contains("use ``` instead of ~~~"));
475 assert_eq!(result[0].line, 1);
476 assert_eq!(result[1].line, 3);
477 }
478
479 #[test]
480 fn test_tilde_style_with_tildes() {
481 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
482 let content = "~~~\ncode\n~~~";
483 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
484 let result = rule.check(&ctx).unwrap();
485
486 assert_eq!(result.len(), 0);
487 }
488
489 #[test]
490 fn test_tilde_style_with_backticks() {
491 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
492 let content = "```\ncode\n```";
493 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
494 let result = rule.check(&ctx).unwrap();
495
496 assert_eq!(result.len(), 2); assert!(result[0].message.contains("use ~~~ instead of ```"));
498 }
499
500 #[test]
501 fn test_consistent_style_tie_prefers_backtick() {
502 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
503 let content = "```\ncode\n```\n\n~~~\nmore code\n~~~";
505 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
506 let result = rule.check(&ctx).unwrap();
507
508 assert_eq!(result.len(), 2);
510 assert_eq!(result[0].line, 5);
511 assert_eq!(result[1].line, 7);
512 }
513
514 #[test]
515 fn test_consistent_style_tilde_most_prevalent() {
516 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
517 let content = "~~~\ncode\n~~~\n\n```\nmore code\n```\n\n~~~\neven more\n~~~";
519 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
520 let result = rule.check(&ctx).unwrap();
521
522 assert_eq!(result.len(), 2);
524 assert_eq!(result[0].line, 5);
525 assert_eq!(result[1].line, 7);
526 }
527
528 #[test]
529 fn test_detect_style_backtick() {
530 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
531 let ctx = LintContext::new("```\ncode\n```", crate::config::MarkdownFlavor::Standard, None);
532 let style = rule.detect_style(&ctx);
533
534 assert_eq!(style, Some(CodeFenceStyle::Backtick));
535 }
536
537 #[test]
538 fn test_detect_style_tilde() {
539 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
540 let ctx = LintContext::new("~~~\ncode\n~~~", crate::config::MarkdownFlavor::Standard, None);
541 let style = rule.detect_style(&ctx);
542
543 assert_eq!(style, Some(CodeFenceStyle::Tilde));
544 }
545
546 #[test]
547 fn test_detect_style_none() {
548 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
549 let ctx = LintContext::new("No code fences here", crate::config::MarkdownFlavor::Standard, None);
550 let style = rule.detect_style(&ctx);
551
552 assert_eq!(style, None);
553 }
554
555 #[test]
556 fn test_fix_backticks_to_tildes() {
557 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
558 let content = "```\ncode\n```";
559 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
560 let fixed = rule.fix(&ctx).unwrap();
561
562 assert_eq!(fixed, "~~~\ncode\n~~~");
563 }
564
565 #[test]
566 fn test_fix_tildes_to_backticks() {
567 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
568 let content = "~~~\ncode\n~~~";
569 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
570 let fixed = rule.fix(&ctx).unwrap();
571
572 assert_eq!(fixed, "```\ncode\n```");
573 }
574
575 #[test]
576 fn test_fix_preserves_fence_length() {
577 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
578 let content = "````\ncode with backtick\n```\ncode\n````";
579 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
580 let fixed = rule.fix(&ctx).unwrap();
581
582 assert_eq!(fixed, "~~~~\ncode with backtick\n```\ncode\n~~~~");
583 }
584
585 #[test]
586 fn test_fix_preserves_language_info() {
587 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
588 let content = "~~~rust\nfn main() {}\n~~~";
589 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
590 let fixed = rule.fix(&ctx).unwrap();
591
592 assert_eq!(fixed, "```rust\nfn main() {}\n```");
593 }
594
595 #[test]
596 fn test_indented_code_fences() {
597 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
598 let content = " ```\n code\n ```";
599 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
600 let result = rule.check(&ctx).unwrap();
601
602 assert_eq!(result.len(), 2);
603 }
604
605 #[test]
606 fn test_fix_indented_fences() {
607 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
608 let content = " ```\n code\n ```";
609 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
610 let fixed = rule.fix(&ctx).unwrap();
611
612 assert_eq!(fixed, " ~~~\n code\n ~~~");
613 }
614
615 #[test]
616 fn test_nested_fences_not_changed() {
617 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
618 let content = "```\ncode with ``` inside\n```";
619 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
620 let fixed = rule.fix(&ctx).unwrap();
621
622 assert_eq!(fixed, "~~~\ncode with ``` inside\n~~~");
623 }
624
625 #[test]
626 fn test_multiple_code_blocks() {
627 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
628 let content = "~~~\ncode1\n~~~\n\nText\n\n~~~python\ncode2\n~~~";
629 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
630 let result = rule.check(&ctx).unwrap();
631
632 assert_eq!(result.len(), 4); }
634
635 #[test]
636 fn test_empty_content() {
637 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
638 let content = "";
639 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
640 let result = rule.check(&ctx).unwrap();
641
642 assert_eq!(result.len(), 0);
643 }
644
645 #[test]
646 fn test_preserve_trailing_newline() {
647 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
648 let content = "~~~\ncode\n~~~\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\n```\n");
653 }
654
655 #[test]
656 fn test_no_trailing_newline() {
657 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
658 let content = "~~~\ncode\n~~~";
659 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
660 let fixed = rule.fix(&ctx).unwrap();
661
662 assert_eq!(fixed, "```\ncode\n```");
663 }
664
665 #[test]
666 fn test_default_config() {
667 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
668 let (name, _config) = rule.default_config_section().unwrap();
669 assert_eq!(name, "MD048");
670 }
671
672 #[test]
675 fn test_tilde_outer_with_backtick_inner_uses_longer_fence() {
676 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
677 let content = "~~~text\n```rust\ncode\n```\n~~~";
678 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
679 let fixed = rule.fix(&ctx).unwrap();
680
681 assert_eq!(fixed, "````text\n```rust\ncode\n```\n````");
683 }
684
685 #[test]
688 fn test_check_tilde_outer_with_backtick_inner_warns_with_correct_replacement() {
689 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
690 let content = "~~~text\n```rust\ncode\n```\n~~~";
691 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
692 let warnings = rule.check(&ctx).unwrap();
693
694 assert_eq!(warnings.len(), 2);
696 let open_fix = warnings[0].fix.as_ref().unwrap();
697 let close_fix = warnings[1].fix.as_ref().unwrap();
698 assert_eq!(open_fix.replacement, "````text");
699 assert_eq!(close_fix.replacement, "````");
700 }
701
702 #[test]
705 fn test_tilde_outer_with_longer_backtick_inner() {
706 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
707 let content = "~~~text\n````rust\ncode\n````\n~~~";
708 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
709 let fixed = rule.fix(&ctx).unwrap();
710
711 assert_eq!(fixed, "`````text\n````rust\ncode\n````\n`````");
712 }
713
714 #[test]
717 fn test_backtick_outer_with_tilde_inner_uses_longer_fence() {
718 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
719 let content = "```text\n~~~rust\ncode\n~~~\n```";
720 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
721 let fixed = rule.fix(&ctx).unwrap();
722
723 assert_eq!(fixed, "~~~~text\n~~~rust\ncode\n~~~\n~~~~");
724 }
725
726 #[test]
734 fn test_info_string_interior_not_ambiguous() {
735 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
736 let content = "```text\n```rust\ncode\n```\n```";
742 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
743 let warnings = rule.check(&ctx).unwrap();
744
745 assert_eq!(warnings.len(), 0, "expected 0 warnings, got {warnings:?}");
748 }
749
750 #[test]
752 fn test_info_string_interior_fix_unchanged() {
753 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
754 let content = "```text\n```rust\ncode\n```\n```";
755 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
756 let fixed = rule.fix(&ctx).unwrap();
757
758 assert_eq!(fixed, content);
760 }
761
762 #[test]
764 fn test_tilde_info_string_interior_not_ambiguous() {
765 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
766 let content = "~~~text\n~~~rust\ncode\n~~~\n~~~";
767 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
768 let fixed = rule.fix(&ctx).unwrap();
769
770 assert_eq!(fixed, content);
772 }
773
774 #[test]
776 fn test_no_ambiguity_when_outer_is_longer() {
777 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
778 let content = "````text\n```rust\ncode\n```\n````";
779 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
780 let warnings = rule.check(&ctx).unwrap();
781
782 assert_eq!(
783 warnings.len(),
784 0,
785 "should have no warnings when outer is already longer"
786 );
787 }
788
789 #[test]
793 fn test_longer_info_string_interior_not_ambiguous() {
794 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
795 let content = "```text\n`````rust\ncode\n`````\n```";
801 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
802 let fixed = rule.fix(&ctx).unwrap();
803
804 assert_eq!(fixed, content);
806 }
807
808 #[test]
810 fn test_info_string_interior_consistent_style_no_warning() {
811 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
812 let content = "```text\n```rust\ncode\n```\n```";
813 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
814 let warnings = rule.check(&ctx).unwrap();
815
816 assert_eq!(warnings.len(), 0);
817 }
818
819 #[test]
826 fn test_cross_style_bare_inner_requires_lengthening() {
827 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
828 let content = "~~~\n`````rust\ncode\n```\n~~~";
832 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
833 let fixed = rule.fix(&ctx).unwrap();
834
835 assert_eq!(fixed, "````\n`````rust\ncode\n```\n````");
838 }
839
840 #[test]
844 fn test_cross_style_info_only_interior_no_lengthening() {
845 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
846 let content = "~~~text\n```rust\nexample\n```rust\n~~~";
850 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
851 let fixed = rule.fix(&ctx).unwrap();
852
853 assert_eq!(fixed, "```text\n```rust\nexample\n```rust\n```");
854 }
855
856 #[test]
859 fn test_same_style_info_outer_shorter_bare_interior_no_warning() {
860 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
861 let content = "````text\n```\nshowing raw fence\n```\n````";
865 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
866 let warnings = rule.check(&ctx).unwrap();
867
868 assert_eq!(
869 warnings.len(),
870 0,
871 "shorter bare interior sequences cannot close a 4-backtick outer"
872 );
873 }
874
875 #[test]
878 fn test_same_style_no_info_outer_shorter_bare_interior_no_warning() {
879 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
880 let content = "````\n```\nsome code\n```\n````";
883 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
884 let warnings = rule.check(&ctx).unwrap();
885
886 assert_eq!(
887 warnings.len(),
888 0,
889 "shorter bare interior sequences cannot close a 4-backtick outer (no info)"
890 );
891 }
892
893 #[test]
896 fn test_overindented_inner_sequence_not_ambiguous() {
897 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
898 let content = "```text\n ```\ncode\n```";
899 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
900 let warnings = rule.check(&ctx).unwrap();
901 let fixed = rule.fix(&ctx).unwrap();
902
903 assert_eq!(warnings.len(), 0, "over-indented inner fence should not warn");
904 assert_eq!(fixed, content, "over-indented inner fence should remain unchanged");
905 }
906
907 #[test]
910 fn test_conversion_ignores_overindented_inner_sequence_for_closing_detection() {
911 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
912 let content = "~~~text\n ~~~\n```rust\ncode\n```\n~~~";
913 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
914 let fixed = rule.fix(&ctx).unwrap();
915
916 assert_eq!(fixed, "````text\n ~~~\n```rust\ncode\n```\n````");
917 }
918
919 #[test]
922 fn test_top_level_four_space_fence_marker_is_ignored() {
923 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
924 let content = " ```\n code\n ```";
925 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
926 let warnings = rule.check(&ctx).unwrap();
927 let fixed = rule.fix(&ctx).unwrap();
928
929 assert_eq!(warnings.len(), 0);
930 assert_eq!(fixed, content);
931 }
932
933 fn assert_fix_roundtrip(rule: &MD048CodeFenceStyle, content: &str) {
939 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
940 let fixed = rule.fix(&ctx).unwrap();
941 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
942 let remaining = rule.check(&ctx2).unwrap();
943 assert!(
944 remaining.is_empty(),
945 "After fix, expected 0 violations but got {}.\nOriginal:\n{content}\nFixed:\n{fixed}\nRemaining: {remaining:?}",
946 remaining.len(),
947 );
948 }
949
950 #[test]
951 fn test_roundtrip_backticks_to_tildes() {
952 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
953 assert_fix_roundtrip(&rule, "```\ncode\n```");
954 }
955
956 #[test]
957 fn test_roundtrip_tildes_to_backticks() {
958 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
959 assert_fix_roundtrip(&rule, "~~~\ncode\n~~~");
960 }
961
962 #[test]
963 fn test_roundtrip_mixed_fences_consistent() {
964 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
965 assert_fix_roundtrip(&rule, "```\ncode\n```\n\n~~~\nmore code\n~~~");
966 }
967
968 #[test]
969 fn test_roundtrip_with_info_string() {
970 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
971 assert_fix_roundtrip(&rule, "~~~rust\nfn main() {}\n~~~");
972 }
973
974 #[test]
975 fn test_roundtrip_longer_fences() {
976 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
977 assert_fix_roundtrip(&rule, "`````\ncode\n`````");
978 }
979
980 #[test]
981 fn test_roundtrip_nested_inner_fences() {
982 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
983 assert_fix_roundtrip(&rule, "~~~text\n```rust\ncode\n```\n~~~");
984 }
985
986 #[test]
987 fn test_roundtrip_indented_fences() {
988 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
989 assert_fix_roundtrip(&rule, " ```\n code\n ```");
990 }
991
992 #[test]
993 fn test_roundtrip_multiple_blocks() {
994 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
995 assert_fix_roundtrip(&rule, "~~~\ncode1\n~~~\n\nText\n\n~~~python\ncode2\n~~~");
996 }
997
998 #[test]
999 fn test_roundtrip_fence_length_ambiguity() {
1000 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
1001 assert_fix_roundtrip(&rule, "~~~\n`````rust\ncode\n```\n~~~");
1002 }
1003
1004 #[test]
1005 fn test_roundtrip_trailing_newline() {
1006 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
1007 assert_fix_roundtrip(&rule, "~~~\ncode\n~~~\n");
1008 }
1009
1010 #[test]
1011 fn test_roundtrip_tilde_outer_longer_backtick_inner() {
1012 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
1013 assert_fix_roundtrip(&rule, "~~~text\n````rust\ncode\n````\n~~~");
1014 }
1015
1016 #[test]
1017 fn test_roundtrip_backtick_outer_tilde_inner() {
1018 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
1019 assert_fix_roundtrip(&rule, "```text\n~~~rust\ncode\n~~~\n```");
1020 }
1021
1022 #[test]
1023 fn test_roundtrip_consistent_tilde_prevalent() {
1024 let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Consistent);
1025 assert_fix_roundtrip(&rule, "~~~\ncode\n~~~\n\n```\nmore code\n```\n\n~~~\neven more\n~~~");
1026 }
1027
1028 #[test]
1032 fn test_fix_idempotent_no_double_blanks_with_nested_fences() {
1033 use crate::fix_coordinator::FixCoordinator;
1034 use crate::rules::Rule;
1035 use crate::rules::md013_line_length::MD013LineLength;
1036
1037 let content = "\
1041- **edition**: Rust edition to use by default for the code snippets. Default is `\"2015\"`. \
1042Individual code blocks can be controlled with the `edition2015`, `edition2018`, `edition2021` \
1043or `edition2024` annotations, such as:
1044
1045 ~~~text
1046 ```rust,edition2015
1047 // This only works in 2015.
1048 let try = true;
1049 ```
1050 ~~~
1051
1052### Build options
1053";
1054 let rules: Vec<Box<dyn Rule>> = vec![
1055 Box::new(MD013LineLength::new(80, false, false, false, true)),
1056 Box::new(MD048CodeFenceStyle::new(CodeFenceStyle::Backtick)),
1057 ];
1058
1059 let mut first_pass = content.to_string();
1060 let coordinator = FixCoordinator::new();
1061 coordinator
1062 .apply_fixes_iterative(&rules, &[], &mut first_pass, &Default::default(), 10, None)
1063 .expect("fix should not fail");
1064
1065 let lines: Vec<&str> = first_pass.lines().collect();
1067 for i in 0..lines.len().saturating_sub(1) {
1068 assert!(
1069 !(lines[i].is_empty() && lines[i + 1].is_empty()),
1070 "Double blank at lines {},{} after first pass:\n{first_pass}",
1071 i + 1,
1072 i + 2
1073 );
1074 }
1075
1076 let mut second_pass = first_pass.clone();
1078 let rules2: Vec<Box<dyn Rule>> = vec![
1079 Box::new(MD013LineLength::new(80, false, false, false, true)),
1080 Box::new(MD048CodeFenceStyle::new(CodeFenceStyle::Backtick)),
1081 ];
1082 let coordinator2 = FixCoordinator::new();
1083 coordinator2
1084 .apply_fixes_iterative(&rules2, &[], &mut second_pass, &Default::default(), 10, None)
1085 .expect("fix should not fail");
1086
1087 assert_eq!(
1088 first_pass, second_pass,
1089 "Fix is not idempotent:\nFirst pass:\n{first_pass}\nSecond pass:\n{second_pass}"
1090 );
1091 }
1092}