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