1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::rules::code_block_utils::CodeBlockStyle;
3use crate::utils::element_cache::ElementCache;
4use crate::utils::mkdocs_admonitions;
5use crate::utils::mkdocs_footnotes;
6use crate::utils::mkdocs_tabs;
7use crate::utils::range_utils::calculate_line_range;
8use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
9use toml;
10
11mod md046_config;
12use md046_config::MD046Config;
13
14#[derive(Clone)]
20pub struct MD046CodeBlockStyle {
21 config: MD046Config,
22}
23
24impl MD046CodeBlockStyle {
25 pub fn new(style: CodeBlockStyle) -> Self {
26 Self {
27 config: MD046Config { style },
28 }
29 }
30
31 pub fn from_config_struct(config: MD046Config) -> Self {
32 Self { config }
33 }
34
35 fn has_valid_fence_indent(line: &str) -> bool {
40 ElementCache::calculate_indentation_width_default(line) < 4
41 }
42
43 fn is_fenced_code_block_start(&self, line: &str) -> bool {
52 if !Self::has_valid_fence_indent(line) {
53 return false;
54 }
55
56 let trimmed = line.trim_start();
57 trimmed.starts_with("```") || trimmed.starts_with("~~~")
58 }
59
60 fn is_list_item(&self, line: &str) -> bool {
61 let trimmed = line.trim_start();
62 (trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ "))
63 || (trimmed.len() > 2
64 && trimmed.chars().next().unwrap().is_numeric()
65 && (trimmed.contains(". ") || trimmed.contains(") ")))
66 }
67
68 fn is_footnote_definition(&self, line: &str) -> bool {
88 let trimmed = line.trim_start();
89 if !trimmed.starts_with("[^") || trimmed.len() < 5 {
90 return false;
91 }
92
93 if let Some(close_bracket_pos) = trimmed.find("]:")
94 && close_bracket_pos > 2
95 {
96 let label = &trimmed[2..close_bracket_pos];
97
98 if label.trim().is_empty() {
99 return false;
100 }
101
102 if label.contains('\r') {
104 return false;
105 }
106
107 if label.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
109 return true;
110 }
111 }
112
113 false
114 }
115
116 fn precompute_block_continuation_context(&self, lines: &[&str]) -> Vec<bool> {
139 let mut in_continuation_context = vec![false; lines.len()];
140 let mut last_list_item_line: Option<usize> = None;
141 let mut last_footnote_line: Option<usize> = None;
142 let mut blank_line_count = 0;
143
144 for (i, line) in lines.iter().enumerate() {
145 let trimmed = line.trim_start();
146 let indent_len = line.len() - trimmed.len();
147
148 if self.is_list_item(line) {
150 last_list_item_line = Some(i);
151 last_footnote_line = None; blank_line_count = 0;
153 in_continuation_context[i] = true;
154 continue;
155 }
156
157 if self.is_footnote_definition(line) {
159 last_footnote_line = Some(i);
160 last_list_item_line = None; blank_line_count = 0;
162 in_continuation_context[i] = true;
163 continue;
164 }
165
166 if line.trim().is_empty() {
168 if last_list_item_line.is_some() || last_footnote_line.is_some() {
170 blank_line_count += 1;
171 in_continuation_context[i] = true;
172
173 }
177 continue;
178 }
179
180 if indent_len == 0 && !trimmed.is_empty() {
182 if trimmed.starts_with('#') {
186 last_list_item_line = None;
187 last_footnote_line = None;
188 blank_line_count = 0;
189 continue;
190 }
191
192 if trimmed.starts_with("---") || trimmed.starts_with("***") {
194 last_list_item_line = None;
195 last_footnote_line = None;
196 blank_line_count = 0;
197 continue;
198 }
199
200 if let Some(list_line) = last_list_item_line
203 && (i - list_line > 5 || blank_line_count > 1)
204 {
205 last_list_item_line = None;
206 }
207
208 if last_footnote_line.is_some() {
210 last_footnote_line = None;
211 }
212
213 blank_line_count = 0;
214
215 if last_list_item_line.is_none() && last_footnote_line.is_some() {
217 last_footnote_line = None;
218 }
219 continue;
220 }
221
222 if indent_len > 0 && (last_list_item_line.is_some() || last_footnote_line.is_some()) {
224 in_continuation_context[i] = true;
225 blank_line_count = 0;
226 }
227 }
228
229 in_continuation_context
230 }
231
232 fn is_indented_code_block_with_context(
234 &self,
235 lines: &[&str],
236 i: usize,
237 is_mkdocs: bool,
238 in_list_context: &[bool],
239 in_tab_context: &[bool],
240 in_admonition_context: &[bool],
241 ) -> bool {
242 if i >= lines.len() {
243 return false;
244 }
245
246 let line = lines[i];
247
248 let indent = ElementCache::calculate_indentation_width_default(line);
250 if indent < 4 {
251 return false;
252 }
253
254 if in_list_context[i] {
256 return false;
257 }
258
259 if is_mkdocs && in_tab_context[i] {
261 return false;
262 }
263
264 if is_mkdocs && in_admonition_context[i] {
267 return false;
268 }
269
270 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
273 let prev_is_indented_code = i > 0
274 && ElementCache::calculate_indentation_width_default(lines[i - 1]) >= 4
275 && !in_list_context[i - 1]
276 && !(is_mkdocs && in_tab_context[i - 1])
277 && !(is_mkdocs && in_admonition_context[i - 1]);
278
279 if !has_blank_line_before && !prev_is_indented_code {
282 return false;
283 }
284
285 true
286 }
287
288 fn precompute_mkdocs_tab_context(&self, lines: &[&str]) -> Vec<bool> {
290 let mut in_tab_context = vec![false; lines.len()];
291 let mut current_tab_indent: Option<usize> = None;
292
293 for (i, line) in lines.iter().enumerate() {
294 if mkdocs_tabs::is_tab_marker(line) {
296 let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
297 current_tab_indent = Some(tab_indent);
298 in_tab_context[i] = true;
299 continue;
300 }
301
302 if let Some(tab_indent) = current_tab_indent {
304 if mkdocs_tabs::is_tab_content(line, tab_indent) {
305 in_tab_context[i] = true;
306 } else if !line.trim().is_empty() && ElementCache::calculate_indentation_width_default(line) < 4 {
307 current_tab_indent = None;
309 } else {
310 in_tab_context[i] = true;
312 }
313 }
314 }
315
316 in_tab_context
317 }
318
319 fn precompute_mkdocs_admonition_context(&self, lines: &[&str]) -> Vec<bool> {
328 let mut in_admonition_context = vec![false; lines.len()];
329 let mut admonition_stack: Vec<usize> = Vec::new();
331
332 for (i, line) in lines.iter().enumerate() {
333 let line_indent = ElementCache::calculate_indentation_width_default(line);
334
335 if mkdocs_admonitions::is_admonition_start(line) {
337 let adm_indent = mkdocs_admonitions::get_admonition_indent(line).unwrap_or(0);
338
339 while let Some(&top_indent) = admonition_stack.last() {
341 if adm_indent <= top_indent {
343 admonition_stack.pop();
344 } else {
345 break;
346 }
347 }
348
349 admonition_stack.push(adm_indent);
351 in_admonition_context[i] = true;
352 continue;
353 }
354
355 if line.trim().is_empty() {
357 if !admonition_stack.is_empty() {
358 in_admonition_context[i] = true;
359 }
360 continue;
361 }
362
363 while let Some(&top_indent) = admonition_stack.last() {
366 if line_indent >= top_indent + 4 {
368 break;
370 } else {
371 admonition_stack.pop();
373 }
374 }
375
376 if !admonition_stack.is_empty() {
378 in_admonition_context[i] = true;
379 }
380 }
381
382 in_admonition_context
383 }
384
385 fn categorize_indented_blocks(
397 &self,
398 lines: &[&str],
399 is_mkdocs: bool,
400 in_list_context: &[bool],
401 in_tab_context: &[bool],
402 in_admonition_context: &[bool],
403 ) -> (Vec<bool>, Vec<bool>) {
404 let mut is_misplaced = vec![false; lines.len()];
405 let mut contains_fences = vec![false; lines.len()];
406
407 let mut i = 0;
409 while i < lines.len() {
410 if !self.is_indented_code_block_with_context(
412 lines,
413 i,
414 is_mkdocs,
415 in_list_context,
416 in_tab_context,
417 in_admonition_context,
418 ) {
419 i += 1;
420 continue;
421 }
422
423 let block_start = i;
425 let mut block_end = i;
426
427 while block_end < lines.len()
428 && self.is_indented_code_block_with_context(
429 lines,
430 block_end,
431 is_mkdocs,
432 in_list_context,
433 in_tab_context,
434 in_admonition_context,
435 )
436 {
437 block_end += 1;
438 }
439
440 if block_end > block_start {
442 let first_line = lines[block_start].trim_start();
443 let last_line = lines[block_end - 1].trim_start();
444
445 let is_backtick_fence = first_line.starts_with("```");
447 let is_tilde_fence = first_line.starts_with("~~~");
448
449 if is_backtick_fence || is_tilde_fence {
450 let fence_char = if is_backtick_fence { '`' } else { '~' };
451 let opener_len = first_line.chars().take_while(|&c| c == fence_char).count();
452
453 let closer_fence_len = last_line.chars().take_while(|&c| c == fence_char).count();
455 let after_closer = &last_line[closer_fence_len..];
456
457 if closer_fence_len >= opener_len && after_closer.trim().is_empty() {
458 is_misplaced[block_start..block_end].fill(true);
460 } else {
461 contains_fences[block_start..block_end].fill(true);
463 }
464 } else {
465 let has_fence_markers = (block_start..block_end).any(|j| {
468 let trimmed = lines[j].trim_start();
469 trimmed.starts_with("```") || trimmed.starts_with("~~~")
470 });
471
472 if has_fence_markers {
473 contains_fences[block_start..block_end].fill(true);
474 }
475 }
476 }
477
478 i = block_end;
479 }
480
481 (is_misplaced, contains_fences)
482 }
483
484 fn check_unclosed_code_blocks(
485 &self,
486 ctx: &crate::lint_context::LintContext,
487 ) -> Result<Vec<LintWarning>, LintError> {
488 let mut warnings = Vec::new();
489 let lines: Vec<&str> = ctx.content.lines().collect();
490
491 let options = Options::all();
493 let parser = Parser::new_ext(ctx.content, options).into_offset_iter();
494
495 let mut code_blocks: Vec<(usize, usize, String, usize, bool, bool)> = Vec::new();
497 let mut current_block_start: Option<(usize, String, usize, bool)> = None;
498
499 for (event, range) in parser {
500 match event {
501 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(info))) => {
502 let line_idx = ctx
504 .line_offsets
505 .iter()
506 .enumerate()
507 .rev()
508 .find(|&(_, offset)| *offset <= range.start)
509 .map(|(idx, _)| idx)
510 .unwrap_or(0);
511
512 let line = lines.get(line_idx).unwrap_or(&"");
514 let trimmed = line.trim();
515
516 let fence_marker = if let Some(pos) = trimmed.find("```") {
518 let count = trimmed[pos..].chars().take_while(|&c| c == '`').count();
519 "`".repeat(count)
520 } else if let Some(pos) = trimmed.find("~~~") {
521 let count = trimmed[pos..].chars().take_while(|&c| c == '~').count();
522 "~".repeat(count)
523 } else {
524 "```".to_string()
525 };
526
527 let lang_info = info.to_string().to_lowercase();
529 let is_markdown_doc = lang_info.starts_with("markdown") || lang_info.starts_with("md");
530
531 current_block_start = Some((range.start, fence_marker, line_idx, is_markdown_doc));
532 }
533 Event::End(TagEnd::CodeBlock) => {
534 if let Some((start, fence_marker, line_idx, is_markdown_doc)) = current_block_start.take() {
535 code_blocks.push((start, range.end, fence_marker, line_idx, true, is_markdown_doc));
536 }
537 }
538 _ => {}
539 }
540 }
541
542 let has_markdown_doc_block = code_blocks.iter().any(|(_, _, _, _, _, is_md)| *is_md);
546
547 if !has_markdown_doc_block {
551 for (block_start, block_end, fence_marker, opening_line_idx, is_fenced, _is_md) in &code_blocks {
552 if !is_fenced {
553 continue;
554 }
555
556 if *block_end != ctx.content.len() {
558 continue;
559 }
560
561 let last_non_empty_line = lines.iter().rev().find(|l| !l.trim().is_empty()).unwrap_or(&"");
564 let trimmed = last_non_empty_line.trim();
565 let fence_char = fence_marker.chars().next().unwrap_or('`');
566
567 let has_closing_fence = if fence_char == '`' {
569 trimmed.starts_with("```") && {
570 let fence_len = trimmed.chars().take_while(|&c| c == '`').count();
571 trimmed[fence_len..].trim().is_empty()
572 }
573 } else {
574 trimmed.starts_with("~~~") && {
575 let fence_len = trimmed.chars().take_while(|&c| c == '~').count();
576 trimmed[fence_len..].trim().is_empty()
577 }
578 };
579
580 if !has_closing_fence {
581 let line = lines.get(*opening_line_idx).unwrap_or(&"");
582 let (start_line, start_col, end_line, end_col) = calculate_line_range(*opening_line_idx + 1, line);
583
584 if let Some(line_info) = ctx.lines.get(*opening_line_idx)
586 && line_info.in_html_comment
587 {
588 continue;
589 }
590
591 warnings.push(LintWarning {
592 rule_name: Some(self.name().to_string()),
593 line: start_line,
594 column: start_col,
595 end_line,
596 end_column: end_col,
597 message: format!("Code block opened with '{fence_marker}' but never closed"),
598 severity: Severity::Warning,
599 fix: Some(Fix {
600 range: (ctx.content.len()..ctx.content.len()),
601 replacement: format!("\n{fence_marker}"),
602 }),
603 });
604 }
605
606 let _ = block_start; }
608 }
609
610 if !has_markdown_doc_block && let Some((_start, fence_marker, line_idx, _is_md)) = current_block_start {
613 let line = lines.get(line_idx).unwrap_or(&"");
614 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_idx + 1, line);
615
616 if let Some(line_info) = ctx.lines.get(line_idx)
618 && line_info.in_html_comment
619 {
620 return Ok(warnings);
621 }
622
623 warnings.push(LintWarning {
624 rule_name: Some(self.name().to_string()),
625 line: start_line,
626 column: start_col,
627 end_line,
628 end_column: end_col,
629 message: format!("Code block opened with '{fence_marker}' but never closed"),
630 severity: Severity::Warning,
631 fix: Some(Fix {
632 range: (ctx.content.len()..ctx.content.len()),
633 replacement: format!("\n{fence_marker}"),
634 }),
635 });
636 }
637
638 if has_markdown_doc_block {
643 return Ok(warnings);
644 }
645
646 for (block_start, block_end, fence_marker, opening_line_idx, is_fenced, is_markdown_doc) in &code_blocks {
647 if !is_fenced {
648 continue;
649 }
650
651 if *is_markdown_doc {
653 continue;
654 }
655
656 let opening_line = lines.get(*opening_line_idx).unwrap_or(&"");
657
658 let fence_char = fence_marker.chars().next().unwrap_or('`');
659 let fence_length = fence_marker.len();
660
661 for (i, line) in lines.iter().enumerate() {
663 let line_start = ctx.line_offsets.get(i).copied().unwrap_or(0);
664 let line_end = ctx.line_offsets.get(i + 1).copied().unwrap_or(ctx.content.len());
665
666 if line_start <= *block_start || line_end >= *block_end {
668 continue;
669 }
670
671 if let Some(line_info) = ctx.lines.get(i)
673 && line_info.in_html_comment
674 {
675 continue;
676 }
677
678 let trimmed = line.trim();
679
680 if (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
682 && trimmed.starts_with(&fence_char.to_string())
683 {
684 let inner_fence_length = trimmed.chars().take_while(|&c| c == fence_char).count();
685 let after_fence = &trimmed[inner_fence_length..];
686
687 if inner_fence_length >= fence_length
689 && !after_fence.trim().is_empty()
690 && !after_fence.contains('`')
691 {
692 let identifier = after_fence.trim();
694 let looks_like_language =
695 identifier.chars().next().is_some_and(|c| c.is_alphabetic() || c == '#')
696 && identifier.len() <= 30
697 && identifier.chars().all(|c| c.is_alphanumeric() || "-_+#. ".contains(c));
698
699 if looks_like_language {
700 let (start_line, start_col, end_line, end_col) =
701 calculate_line_range(*opening_line_idx + 1, opening_line);
702
703 let line_start_byte = ctx.line_index.get_line_start_byte(i + 1).unwrap_or(0);
704
705 warnings.push(LintWarning {
706 rule_name: Some(self.name().to_string()),
707 line: start_line,
708 column: start_col,
709 end_line,
710 end_column: end_col,
711 message: format!(
712 "Code block '{fence_marker}' should be closed before starting new one at line {}",
713 i + 1
714 ),
715 severity: Severity::Warning,
716 fix: Some(Fix {
717 range: (line_start_byte..line_start_byte),
718 replacement: format!("{fence_marker}\n\n"),
719 }),
720 });
721
722 break; }
724 }
725 }
726 }
727 }
728
729 Ok(warnings)
730 }
731
732 fn detect_style(&self, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
733 if content.is_empty() {
735 return None;
736 }
737
738 let lines: Vec<&str> = content.lines().collect();
739 let mut fenced_count = 0;
740 let mut indented_count = 0;
741
742 let in_list_context = self.precompute_block_continuation_context(&lines);
744 let in_tab_context = if is_mkdocs {
745 self.precompute_mkdocs_tab_context(&lines)
746 } else {
747 vec![false; lines.len()]
748 };
749 let in_admonition_context = if is_mkdocs {
750 self.precompute_mkdocs_admonition_context(&lines)
751 } else {
752 vec![false; lines.len()]
753 };
754
755 let mut in_fenced = false;
757 let mut prev_was_indented = false;
758
759 for (i, line) in lines.iter().enumerate() {
760 if self.is_fenced_code_block_start(line) {
761 if !in_fenced {
762 fenced_count += 1;
764 in_fenced = true;
765 } else {
766 in_fenced = false;
768 }
769 } else if !in_fenced
770 && self.is_indented_code_block_with_context(
771 &lines,
772 i,
773 is_mkdocs,
774 &in_list_context,
775 &in_tab_context,
776 &in_admonition_context,
777 )
778 {
779 if !prev_was_indented {
781 indented_count += 1;
782 }
783 prev_was_indented = true;
784 } else {
785 prev_was_indented = false;
786 }
787 }
788
789 if fenced_count == 0 && indented_count == 0 {
790 None
792 } else if fenced_count > 0 && indented_count == 0 {
793 Some(CodeBlockStyle::Fenced)
795 } else if fenced_count == 0 && indented_count > 0 {
796 Some(CodeBlockStyle::Indented)
798 } else {
799 if fenced_count >= indented_count {
802 Some(CodeBlockStyle::Fenced)
803 } else {
804 Some(CodeBlockStyle::Indented)
805 }
806 }
807 }
808}
809
810impl Rule for MD046CodeBlockStyle {
811 fn name(&self) -> &'static str {
812 "MD046"
813 }
814
815 fn description(&self) -> &'static str {
816 "Code blocks should use a consistent style"
817 }
818
819 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
820 if ctx.content.is_empty() {
822 return Ok(Vec::new());
823 }
824
825 if !ctx.content.contains("```")
827 && !ctx.content.contains("~~~")
828 && !ctx.content.contains(" ")
829 && !ctx.content.contains('\t')
830 {
831 return Ok(Vec::new());
832 }
833
834 let unclosed_warnings = self.check_unclosed_code_blocks(ctx)?;
836
837 if !unclosed_warnings.is_empty() {
839 return Ok(unclosed_warnings);
840 }
841
842 let lines: Vec<&str> = ctx.content.lines().collect();
844 let mut warnings = Vec::new();
845
846 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
848
849 let target_style = match self.config.style {
851 CodeBlockStyle::Consistent => self
852 .detect_style(ctx.content, is_mkdocs)
853 .unwrap_or(CodeBlockStyle::Fenced),
854 _ => self.config.style,
855 };
856
857 let in_tab_context = if is_mkdocs {
859 self.precompute_mkdocs_tab_context(&lines)
860 } else {
861 vec![false; lines.len()]
862 };
863 let in_admonition_context = if is_mkdocs {
864 self.precompute_mkdocs_admonition_context(&lines)
865 } else {
866 vec![false; lines.len()]
867 };
868
869 let mut in_fenced_block = vec![false; lines.len()];
872 let mut reported_indented_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
873
874 let options = Options::all();
875 let parser = Parser::new_ext(ctx.content, options).into_offset_iter();
876
877 for (event, range) in parser {
878 let start = range.start;
879 let end = range.end;
880
881 if start >= ctx.content.len() || end > ctx.content.len() {
882 continue;
883 }
884
885 let start_line_idx = ctx
887 .line_offsets
888 .iter()
889 .enumerate()
890 .rev()
891 .find(|&(_, &offset)| offset <= start)
892 .map(|(idx, _)| idx)
893 .unwrap_or(0);
894
895 match event {
896 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) => {
897 for (line_idx, line_info) in ctx.lines.iter().enumerate() {
899 if line_info.byte_offset >= start && line_info.byte_offset < end {
900 in_fenced_block[line_idx] = true;
901 }
902 }
903
904 if target_style == CodeBlockStyle::Indented {
906 let line = lines.get(start_line_idx).unwrap_or(&"");
907
908 if ctx.lines.get(start_line_idx).is_some_and(|info| info.in_html_comment) {
910 continue;
911 }
912
913 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
914 warnings.push(LintWarning {
915 rule_name: Some(self.name().to_string()),
916 line: start_line,
917 column: start_col,
918 end_line,
919 end_column: end_col,
920 message: "Use indented code blocks".to_string(),
921 severity: Severity::Warning,
922 fix: Some(Fix {
923 range: ctx.line_index.line_col_to_byte_range(start_line_idx + 1, 1),
924 replacement: String::new(),
925 }),
926 });
927 }
928 }
929 Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) => {
930 if target_style == CodeBlockStyle::Fenced && !reported_indented_lines.contains(&start_line_idx) {
934 let line = lines.get(start_line_idx).unwrap_or(&"");
935
936 if ctx.lines.get(start_line_idx).is_some_and(|info| {
939 info.in_html_comment || info.in_mkdocstrings || info.blockquote.is_some()
940 }) {
941 continue;
942 }
943
944 if mkdocs_footnotes::is_within_footnote_definition(ctx.content, start) {
946 continue;
947 }
948
949 if is_mkdocs && in_tab_context.get(start_line_idx).copied().unwrap_or(false) {
951 continue;
952 }
953
954 if is_mkdocs && in_admonition_context.get(start_line_idx).copied().unwrap_or(false) {
956 continue;
957 }
958
959 reported_indented_lines.insert(start_line_idx);
960
961 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
962 warnings.push(LintWarning {
963 rule_name: Some(self.name().to_string()),
964 line: start_line,
965 column: start_col,
966 end_line,
967 end_column: end_col,
968 message: "Use fenced code blocks".to_string(),
969 severity: Severity::Warning,
970 fix: Some(Fix {
971 range: ctx.line_index.line_col_to_byte_range(start_line_idx + 1, 1),
972 replacement: format!("```\n{}", line.trim_start()),
973 }),
974 });
975 }
976 }
977 _ => {}
978 }
979 }
980
981 warnings.sort_by_key(|w| (w.line, w.column));
983
984 Ok(warnings)
985 }
986
987 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
988 let content = ctx.content;
989 if content.is_empty() {
990 return Ok(String::new());
991 }
992
993 let unclosed_warnings = self.check_unclosed_code_blocks(ctx)?;
995
996 if !unclosed_warnings.is_empty() {
998 for warning in &unclosed_warnings {
1000 if warning
1001 .message
1002 .contains("should be closed before starting new one at line")
1003 {
1004 if let Some(fix) = &warning.fix {
1006 let mut result = String::new();
1007 result.push_str(&content[..fix.range.start]);
1008 result.push_str(&fix.replacement);
1009 result.push_str(&content[fix.range.start..]);
1010 return Ok(result);
1011 }
1012 }
1013 }
1014 }
1015
1016 let lines: Vec<&str> = content.lines().collect();
1017
1018 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
1020 let target_style = match self.config.style {
1021 CodeBlockStyle::Consistent => self.detect_style(content, is_mkdocs).unwrap_or(CodeBlockStyle::Fenced),
1022 _ => self.config.style,
1023 };
1024
1025 let in_list_context = self.precompute_block_continuation_context(&lines);
1027 let in_tab_context = if is_mkdocs {
1028 self.precompute_mkdocs_tab_context(&lines)
1029 } else {
1030 vec![false; lines.len()]
1031 };
1032 let in_admonition_context = if is_mkdocs {
1033 self.precompute_mkdocs_admonition_context(&lines)
1034 } else {
1035 vec![false; lines.len()]
1036 };
1037
1038 let (misplaced_fence_lines, unsafe_fence_lines) = self.categorize_indented_blocks(
1042 &lines,
1043 is_mkdocs,
1044 &in_list_context,
1045 &in_tab_context,
1046 &in_admonition_context,
1047 );
1048
1049 let mut result = String::with_capacity(content.len());
1050 let mut in_fenced_block = false;
1051 let mut fenced_fence_type = None;
1052 let mut in_indented_block = false;
1053
1054 for (i, line) in lines.iter().enumerate() {
1055 let trimmed = line.trim_start();
1056
1057 if !in_fenced_block
1060 && Self::has_valid_fence_indent(line)
1061 && (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
1062 {
1063 in_fenced_block = true;
1064 fenced_fence_type = Some(if trimmed.starts_with("```") { "```" } else { "~~~" });
1065
1066 if target_style == CodeBlockStyle::Indented {
1067 in_indented_block = true;
1069 } else {
1070 result.push_str(line);
1072 result.push('\n');
1073 }
1074 } else if in_fenced_block && fenced_fence_type.is_some() {
1075 let fence = fenced_fence_type.unwrap();
1076 if trimmed.starts_with(fence) {
1077 in_fenced_block = false;
1078 fenced_fence_type = None;
1079 in_indented_block = false;
1080
1081 if target_style == CodeBlockStyle::Indented {
1082 } else {
1084 result.push_str(line);
1086 result.push('\n');
1087 }
1088 } else if target_style == CodeBlockStyle::Indented {
1089 result.push_str(" ");
1093 result.push_str(line);
1094 result.push('\n');
1095 } else {
1096 result.push_str(line);
1098 result.push('\n');
1099 }
1100 } else if self.is_indented_code_block_with_context(
1101 &lines,
1102 i,
1103 is_mkdocs,
1104 &in_list_context,
1105 &in_tab_context,
1106 &in_admonition_context,
1107 ) {
1108 let prev_line_is_indented = i > 0
1112 && self.is_indented_code_block_with_context(
1113 &lines,
1114 i - 1,
1115 is_mkdocs,
1116 &in_list_context,
1117 &in_tab_context,
1118 &in_admonition_context,
1119 );
1120
1121 if target_style == CodeBlockStyle::Fenced {
1122 let trimmed_content = line.trim_start();
1123
1124 if misplaced_fence_lines[i] {
1127 result.push_str(trimmed_content);
1129 result.push('\n');
1130 } else if unsafe_fence_lines[i] {
1131 result.push_str(line);
1134 result.push('\n');
1135 } else if !prev_line_is_indented && !in_indented_block {
1136 result.push_str("```\n");
1138 result.push_str(trimmed_content);
1139 result.push('\n');
1140 in_indented_block = true;
1141 } else {
1142 result.push_str(trimmed_content);
1144 result.push('\n');
1145 }
1146
1147 let next_line_is_indented = i < lines.len() - 1
1149 && self.is_indented_code_block_with_context(
1150 &lines,
1151 i + 1,
1152 is_mkdocs,
1153 &in_list_context,
1154 &in_tab_context,
1155 &in_admonition_context,
1156 );
1157 if !next_line_is_indented
1159 && in_indented_block
1160 && !misplaced_fence_lines[i]
1161 && !unsafe_fence_lines[i]
1162 {
1163 result.push_str("```\n");
1164 in_indented_block = false;
1165 }
1166 } else {
1167 result.push_str(line);
1169 result.push('\n');
1170 }
1171 } else {
1172 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1174 result.push_str("```\n");
1175 in_indented_block = false;
1176 }
1177
1178 result.push_str(line);
1179 result.push('\n');
1180 }
1181 }
1182
1183 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1185 result.push_str("```\n");
1186 }
1187
1188 if let Some(fence_type) = fenced_fence_type
1190 && in_fenced_block
1191 {
1192 result.push_str(fence_type);
1193 result.push('\n');
1194 }
1195
1196 if !content.ends_with('\n') && result.ends_with('\n') {
1198 result.pop();
1199 }
1200
1201 Ok(result)
1202 }
1203
1204 fn category(&self) -> RuleCategory {
1206 RuleCategory::CodeBlock
1207 }
1208
1209 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
1211 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
1214 }
1215
1216 fn as_any(&self) -> &dyn std::any::Any {
1217 self
1218 }
1219
1220 fn default_config_section(&self) -> Option<(String, toml::Value)> {
1221 let json_value = serde_json::to_value(&self.config).ok()?;
1222 Some((
1223 self.name().to_string(),
1224 crate::rule_config_serde::json_to_toml_value(&json_value)?,
1225 ))
1226 }
1227
1228 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1229 where
1230 Self: Sized,
1231 {
1232 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
1233 Box::new(Self::from_config_struct(rule_config))
1234 }
1235}
1236
1237#[cfg(test)]
1238mod tests {
1239 use super::*;
1240 use crate::lint_context::LintContext;
1241
1242 #[test]
1243 fn test_fenced_code_block_detection() {
1244 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1245 assert!(rule.is_fenced_code_block_start("```"));
1246 assert!(rule.is_fenced_code_block_start("```rust"));
1247 assert!(rule.is_fenced_code_block_start("~~~"));
1248 assert!(rule.is_fenced_code_block_start("~~~python"));
1249 assert!(rule.is_fenced_code_block_start(" ```"));
1250 assert!(!rule.is_fenced_code_block_start("``"));
1251 assert!(!rule.is_fenced_code_block_start("~~"));
1252 assert!(!rule.is_fenced_code_block_start("Regular text"));
1253 }
1254
1255 #[test]
1256 fn test_consistent_style_with_fenced_blocks() {
1257 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1258 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
1259 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1260 let result = rule.check(&ctx).unwrap();
1261
1262 assert_eq!(result.len(), 0);
1264 }
1265
1266 #[test]
1267 fn test_consistent_style_with_indented_blocks() {
1268 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1269 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
1270 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1271 let result = rule.check(&ctx).unwrap();
1272
1273 assert_eq!(result.len(), 0);
1275 }
1276
1277 #[test]
1278 fn test_consistent_style_mixed() {
1279 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1280 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
1281 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1282 let result = rule.check(&ctx).unwrap();
1283
1284 assert!(!result.is_empty());
1286 }
1287
1288 #[test]
1289 fn test_fenced_style_with_indented_blocks() {
1290 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1291 let content = "Text\n\n indented code\n more code\n\nMore text";
1292 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1293 let result = rule.check(&ctx).unwrap();
1294
1295 assert!(!result.is_empty());
1297 assert!(result[0].message.contains("Use fenced code blocks"));
1298 }
1299
1300 #[test]
1301 fn test_fenced_style_with_tab_indented_blocks() {
1302 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1303 let content = "Text\n\n\ttab indented code\n\tmore code\n\nMore text";
1304 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1305 let result = rule.check(&ctx).unwrap();
1306
1307 assert!(!result.is_empty());
1309 assert!(result[0].message.contains("Use fenced code blocks"));
1310 }
1311
1312 #[test]
1313 fn test_fenced_style_with_mixed_whitespace_indented_blocks() {
1314 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1315 let content = "Text\n\n \tmixed indent code\n \tmore code\n\nMore text";
1317 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1318 let result = rule.check(&ctx).unwrap();
1319
1320 assert!(
1322 !result.is_empty(),
1323 "Mixed whitespace (2 spaces + tab) should be detected as indented code"
1324 );
1325 assert!(result[0].message.contains("Use fenced code blocks"));
1326 }
1327
1328 #[test]
1329 fn test_fenced_style_with_one_space_tab_indent() {
1330 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1331 let content = "Text\n\n \ttab after one space\n \tmore code\n\nMore text";
1333 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1334 let result = rule.check(&ctx).unwrap();
1335
1336 assert!(!result.is_empty(), "1 space + tab should be detected as indented code");
1337 assert!(result[0].message.contains("Use fenced code blocks"));
1338 }
1339
1340 #[test]
1341 fn test_indented_style_with_fenced_blocks() {
1342 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1343 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
1344 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1345 let result = rule.check(&ctx).unwrap();
1346
1347 assert!(!result.is_empty());
1349 assert!(result[0].message.contains("Use indented code blocks"));
1350 }
1351
1352 #[test]
1353 fn test_unclosed_code_block() {
1354 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1355 let content = "```\ncode without closing fence";
1356 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1357 let result = rule.check(&ctx).unwrap();
1358
1359 assert_eq!(result.len(), 1);
1360 assert!(result[0].message.contains("never closed"));
1361 }
1362
1363 #[test]
1364 fn test_nested_code_blocks() {
1365 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1366 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
1367 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1368 let result = rule.check(&ctx).unwrap();
1369
1370 assert_eq!(result.len(), 0);
1372 }
1373
1374 #[test]
1375 fn test_fix_indented_to_fenced() {
1376 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1377 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
1378 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1379 let fixed = rule.fix(&ctx).unwrap();
1380
1381 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
1382 }
1383
1384 #[test]
1385 fn test_fix_fenced_to_indented() {
1386 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1387 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
1388 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1389 let fixed = rule.fix(&ctx).unwrap();
1390
1391 assert!(fixed.contains(" code line 1\n code line 2"));
1392 assert!(!fixed.contains("```"));
1393 }
1394
1395 #[test]
1396 fn test_fix_fenced_to_indented_preserves_internal_indentation() {
1397 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1400 let content = r#"# Test
1401
1402```html
1403<!doctype html>
1404<html>
1405 <head>
1406 <title>Test</title>
1407 </head>
1408</html>
1409```
1410"#;
1411 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1412 let fixed = rule.fix(&ctx).unwrap();
1413
1414 assert!(
1417 fixed.contains(" <head>"),
1418 "Expected 6 spaces before <head> (4 for code block + 2 original), got:\n{fixed}"
1419 );
1420 assert!(
1421 fixed.contains(" <title>"),
1422 "Expected 8 spaces before <title> (4 for code block + 4 original), got:\n{fixed}"
1423 );
1424 assert!(!fixed.contains("```"), "Fenced markers should be removed");
1425 }
1426
1427 #[test]
1428 fn test_fix_fenced_to_indented_preserves_python_indentation() {
1429 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1431 let content = r#"# Python Example
1432
1433```python
1434def greet(name):
1435 if name:
1436 print(f"Hello, {name}!")
1437 else:
1438 print("Hello, World!")
1439```
1440"#;
1441 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1442 let fixed = rule.fix(&ctx).unwrap();
1443
1444 assert!(
1446 fixed.contains(" def greet(name):"),
1447 "Function def should have 4 spaces (code block indent)"
1448 );
1449 assert!(
1450 fixed.contains(" if name:"),
1451 "if statement should have 8 spaces (4 code + 4 Python)"
1452 );
1453 assert!(
1454 fixed.contains(" print"),
1455 "print should have 12 spaces (4 code + 8 Python)"
1456 );
1457 }
1458
1459 #[test]
1460 fn test_fix_fenced_to_indented_preserves_yaml_indentation() {
1461 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1463 let content = r#"# Config
1464
1465```yaml
1466server:
1467 host: localhost
1468 port: 8080
1469 ssl:
1470 enabled: true
1471 cert: /path/to/cert
1472```
1473"#;
1474 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1475 let fixed = rule.fix(&ctx).unwrap();
1476
1477 assert!(fixed.contains(" server:"), "Root key should have 4 spaces");
1478 assert!(fixed.contains(" host:"), "First level should have 6 spaces");
1479 assert!(fixed.contains(" ssl:"), "ssl key should have 6 spaces");
1480 assert!(fixed.contains(" enabled:"), "Nested ssl should have 8 spaces");
1481 }
1482
1483 #[test]
1484 fn test_fix_fenced_to_indented_preserves_empty_lines() {
1485 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1487 let content = "```\nline1\n\nline2\n```\n";
1488 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1489 let fixed = rule.fix(&ctx).unwrap();
1490
1491 assert!(fixed.contains(" line1"), "line1 should be indented");
1493 assert!(fixed.contains(" line2"), "line2 should be indented");
1494 }
1496
1497 #[test]
1498 fn test_fix_fenced_to_indented_multiple_blocks() {
1499 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1501 let content = r#"# Doc
1502
1503```python
1504def foo():
1505 pass
1506```
1507
1508Text between.
1509
1510```yaml
1511key:
1512 value: 1
1513```
1514"#;
1515 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1516 let fixed = rule.fix(&ctx).unwrap();
1517
1518 assert!(fixed.contains(" def foo():"), "Python def should be indented");
1519 assert!(fixed.contains(" pass"), "Python body should have 8 spaces");
1520 assert!(fixed.contains(" key:"), "YAML root should have 4 spaces");
1521 assert!(fixed.contains(" value:"), "YAML nested should have 6 spaces");
1522 assert!(!fixed.contains("```"), "No fence markers should remain");
1523 }
1524
1525 #[test]
1526 fn test_fix_unclosed_block() {
1527 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1528 let content = "```\ncode without closing";
1529 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1530 let fixed = rule.fix(&ctx).unwrap();
1531
1532 assert!(fixed.ends_with("```"));
1534 }
1535
1536 #[test]
1537 fn test_code_block_in_list() {
1538 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1539 let content = "- List item\n code in list\n more code\n- Next item";
1540 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1541 let result = rule.check(&ctx).unwrap();
1542
1543 assert_eq!(result.len(), 0);
1545 }
1546
1547 #[test]
1548 fn test_detect_style_fenced() {
1549 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1550 let content = "```\ncode\n```";
1551 let style = rule.detect_style(content, false);
1552
1553 assert_eq!(style, Some(CodeBlockStyle::Fenced));
1554 }
1555
1556 #[test]
1557 fn test_detect_style_indented() {
1558 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1559 let content = "Text\n\n code\n\nMore";
1560 let style = rule.detect_style(content, false);
1561
1562 assert_eq!(style, Some(CodeBlockStyle::Indented));
1563 }
1564
1565 #[test]
1566 fn test_detect_style_none() {
1567 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1568 let content = "No code blocks here";
1569 let style = rule.detect_style(content, false);
1570
1571 assert_eq!(style, None);
1572 }
1573
1574 #[test]
1575 fn test_tilde_fence() {
1576 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1577 let content = "~~~\ncode\n~~~";
1578 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1579 let result = rule.check(&ctx).unwrap();
1580
1581 assert_eq!(result.len(), 0);
1583 }
1584
1585 #[test]
1586 fn test_language_specification() {
1587 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1588 let content = "```rust\nfn main() {}\n```";
1589 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1590 let result = rule.check(&ctx).unwrap();
1591
1592 assert_eq!(result.len(), 0);
1593 }
1594
1595 #[test]
1596 fn test_empty_content() {
1597 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1598 let content = "";
1599 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1600 let result = rule.check(&ctx).unwrap();
1601
1602 assert_eq!(result.len(), 0);
1603 }
1604
1605 #[test]
1606 fn test_default_config() {
1607 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1608 let (name, _config) = rule.default_config_section().unwrap();
1609 assert_eq!(name, "MD046");
1610 }
1611
1612 #[test]
1613 fn test_markdown_documentation_block() {
1614 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1615 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
1616 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1617 let result = rule.check(&ctx).unwrap();
1618
1619 assert_eq!(result.len(), 0);
1621 }
1622
1623 #[test]
1624 fn test_preserve_trailing_newline() {
1625 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1626 let content = "```\ncode\n```\n";
1627 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1628 let fixed = rule.fix(&ctx).unwrap();
1629
1630 assert_eq!(fixed, content);
1631 }
1632
1633 #[test]
1634 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
1635 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1636 let content = r#"# Document
1637
1638=== "Python"
1639
1640 This is tab content
1641 Not an indented code block
1642
1643 ```python
1644 def hello():
1645 print("Hello")
1646 ```
1647
1648=== "JavaScript"
1649
1650 More tab content here
1651 Also not an indented code block"#;
1652
1653 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1654 let result = rule.check(&ctx).unwrap();
1655
1656 assert_eq!(result.len(), 0);
1658 }
1659
1660 #[test]
1661 fn test_mkdocs_tabs_with_actual_indented_code() {
1662 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1663 let content = r#"# Document
1664
1665=== "Tab 1"
1666
1667 This is tab content
1668
1669Regular text
1670
1671 This is an actual indented code block
1672 Should be flagged"#;
1673
1674 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1675 let result = rule.check(&ctx).unwrap();
1676
1677 assert_eq!(result.len(), 1);
1679 assert!(result[0].message.contains("Use fenced code blocks"));
1680 }
1681
1682 #[test]
1683 fn test_mkdocs_tabs_detect_style() {
1684 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1685 let content = r#"=== "Tab 1"
1686
1687 Content in tab
1688 More content
1689
1690=== "Tab 2"
1691
1692 Content in second tab"#;
1693
1694 let style = rule.detect_style(content, true);
1696 assert_eq!(style, None); let style = rule.detect_style(content, false);
1700 assert_eq!(style, Some(CodeBlockStyle::Indented));
1701 }
1702
1703 #[test]
1704 fn test_mkdocs_nested_tabs() {
1705 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1706 let content = r#"# Document
1707
1708=== "Outer Tab"
1709
1710 Some content
1711
1712 === "Nested Tab"
1713
1714 Nested tab content
1715 Should not be flagged"#;
1716
1717 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1718 let result = rule.check(&ctx).unwrap();
1719
1720 assert_eq!(result.len(), 0);
1722 }
1723
1724 #[test]
1725 fn test_mkdocs_admonitions_not_flagged_as_indented_code() {
1726 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1729 let content = r#"# Document
1730
1731!!! note
1732 This is normal admonition content, not a code block.
1733 It spans multiple lines.
1734
1735??? warning "Collapsible Warning"
1736 This is also admonition content.
1737
1738???+ tip "Expanded Tip"
1739 And this one too.
1740
1741Regular text outside admonitions."#;
1742
1743 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1744 let result = rule.check(&ctx).unwrap();
1745
1746 assert_eq!(
1748 result.len(),
1749 0,
1750 "Admonition content in MkDocs mode should not trigger MD046"
1751 );
1752 }
1753
1754 #[test]
1755 fn test_mkdocs_admonition_with_actual_indented_code() {
1756 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1758 let content = r#"# Document
1759
1760!!! note
1761 This is admonition content.
1762
1763Regular text ends the admonition.
1764
1765 This is actual indented code (should be flagged)"#;
1766
1767 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1768 let result = rule.check(&ctx).unwrap();
1769
1770 assert_eq!(result.len(), 1);
1772 assert!(result[0].message.contains("Use fenced code blocks"));
1773 }
1774
1775 #[test]
1776 fn test_admonition_in_standard_mode_flagged() {
1777 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1781 let content = r#"# Document
1782
1783!!! note
1784
1785 This looks like code in standard mode.
1786
1787Regular text."#;
1788
1789 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1791 let result = rule.check(&ctx).unwrap();
1792
1793 assert_eq!(
1795 result.len(),
1796 1,
1797 "Admonition content in Standard mode should be flagged as indented code"
1798 );
1799 }
1800
1801 #[test]
1802 fn test_mkdocs_admonition_with_fenced_code_inside() {
1803 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1805 let content = r#"# Document
1806
1807!!! note "Code Example"
1808 Here's some code:
1809
1810 ```python
1811 def hello():
1812 print("world")
1813 ```
1814
1815 More text after code.
1816
1817Regular text."#;
1818
1819 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1820 let result = rule.check(&ctx).unwrap();
1821
1822 assert_eq!(result.len(), 0, "Fenced code blocks inside admonitions should be valid");
1824 }
1825
1826 #[test]
1827 fn test_mkdocs_nested_admonitions() {
1828 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1830 let content = r#"# Document
1831
1832!!! note "Outer"
1833 Outer content.
1834
1835 !!! warning "Inner"
1836 Inner content.
1837 More inner content.
1838
1839 Back to outer.
1840
1841Regular text."#;
1842
1843 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1844 let result = rule.check(&ctx).unwrap();
1845
1846 assert_eq!(result.len(), 0, "Nested admonitions should not be flagged");
1848 }
1849
1850 #[test]
1851 fn test_mkdocs_admonition_fix_does_not_wrap() {
1852 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1854 let content = r#"!!! note
1855 Content that should stay as admonition content.
1856 Not be wrapped in code fences.
1857"#;
1858
1859 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1860 let fixed = rule.fix(&ctx).unwrap();
1861
1862 assert!(
1864 !fixed.contains("```\n Content"),
1865 "Admonition content should not be wrapped in fences"
1866 );
1867 assert_eq!(fixed, content, "Content should remain unchanged");
1868 }
1869
1870 #[test]
1871 fn test_mkdocs_empty_admonition() {
1872 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1874 let content = r#"!!! note
1875
1876Regular paragraph after empty admonition.
1877
1878 This IS an indented code block (after blank + non-indented line)."#;
1879
1880 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1881 let result = rule.check(&ctx).unwrap();
1882
1883 assert_eq!(result.len(), 1, "Indented code after admonition ends should be flagged");
1885 }
1886
1887 #[test]
1888 fn test_mkdocs_indented_admonition() {
1889 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1891 let content = r#"- List item
1892
1893 !!! note
1894 Indented admonition content.
1895 More content.
1896
1897- Next item"#;
1898
1899 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1900 let result = rule.check(&ctx).unwrap();
1901
1902 assert_eq!(
1904 result.len(),
1905 0,
1906 "Indented admonitions (e.g., in lists) should not be flagged"
1907 );
1908 }
1909
1910 #[test]
1911 fn test_footnote_indented_paragraphs_not_flagged() {
1912 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1913 let content = r#"# Test Document with Footnotes
1914
1915This is some text with a footnote[^1].
1916
1917Here's some code:
1918
1919```bash
1920echo "fenced code block"
1921```
1922
1923More text with another footnote[^2].
1924
1925[^1]: Really interesting footnote text.
1926
1927 Even more interesting second paragraph.
1928
1929[^2]: Another footnote.
1930
1931 With a second paragraph too.
1932
1933 And even a third paragraph!"#;
1934
1935 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1936 let result = rule.check(&ctx).unwrap();
1937
1938 assert_eq!(result.len(), 0);
1940 }
1941
1942 #[test]
1943 fn test_footnote_definition_detection() {
1944 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1945
1946 assert!(rule.is_footnote_definition("[^1]: Footnote text"));
1949 assert!(rule.is_footnote_definition("[^foo]: Footnote text"));
1950 assert!(rule.is_footnote_definition("[^long-name]: Footnote text"));
1951 assert!(rule.is_footnote_definition("[^test_123]: Mixed chars"));
1952 assert!(rule.is_footnote_definition(" [^1]: Indented footnote"));
1953 assert!(rule.is_footnote_definition("[^a]: Minimal valid footnote"));
1954 assert!(rule.is_footnote_definition("[^123]: Numeric label"));
1955 assert!(rule.is_footnote_definition("[^_]: Single underscore"));
1956 assert!(rule.is_footnote_definition("[^-]: Single hyphen"));
1957
1958 assert!(!rule.is_footnote_definition("[^]: No label"));
1960 assert!(!rule.is_footnote_definition("[^ ]: Whitespace only"));
1961 assert!(!rule.is_footnote_definition("[^ ]: Multiple spaces"));
1962 assert!(!rule.is_footnote_definition("[^\t]: Tab only"));
1963
1964 assert!(!rule.is_footnote_definition("[^]]: Extra bracket"));
1966 assert!(!rule.is_footnote_definition("Regular text [^1]:"));
1967 assert!(!rule.is_footnote_definition("[1]: Not a footnote"));
1968 assert!(!rule.is_footnote_definition("[^")); assert!(!rule.is_footnote_definition("[^1:")); assert!(!rule.is_footnote_definition("^1]: Missing opening bracket"));
1971
1972 assert!(!rule.is_footnote_definition("[^test.name]: Period"));
1974 assert!(!rule.is_footnote_definition("[^test name]: Space in label"));
1975 assert!(!rule.is_footnote_definition("[^test@name]: Special char"));
1976 assert!(!rule.is_footnote_definition("[^test/name]: Slash"));
1977 assert!(!rule.is_footnote_definition("[^test\\name]: Backslash"));
1978
1979 assert!(!rule.is_footnote_definition("[^test\r]: Carriage return"));
1982 }
1983
1984 #[test]
1985 fn test_footnote_with_blank_lines() {
1986 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1990 let content = r#"# Document
1991
1992Text with footnote[^1].
1993
1994[^1]: First paragraph.
1995
1996 Second paragraph after blank line.
1997
1998 Third paragraph after another blank line.
1999
2000Regular text at column 0 ends the footnote."#;
2001
2002 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2003 let result = rule.check(&ctx).unwrap();
2004
2005 assert_eq!(
2007 result.len(),
2008 0,
2009 "Indented content within footnotes should not trigger MD046"
2010 );
2011 }
2012
2013 #[test]
2014 fn test_footnote_multiple_consecutive_blank_lines() {
2015 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2018 let content = r#"Text[^1].
2019
2020[^1]: First paragraph.
2021
2022
2023
2024 Content after three blank lines (still part of footnote).
2025
2026Not indented, so footnote ends here."#;
2027
2028 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2029 let result = rule.check(&ctx).unwrap();
2030
2031 assert_eq!(
2033 result.len(),
2034 0,
2035 "Multiple blank lines shouldn't break footnote continuation"
2036 );
2037 }
2038
2039 #[test]
2040 fn test_footnote_terminated_by_non_indented_content() {
2041 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2044 let content = r#"[^1]: Footnote content.
2045
2046 More indented content in footnote.
2047
2048This paragraph is not indented, so footnote ends.
2049
2050 This should be flagged as indented code block."#;
2051
2052 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2053 let result = rule.check(&ctx).unwrap();
2054
2055 assert_eq!(
2057 result.len(),
2058 1,
2059 "Indented code after footnote termination should be flagged"
2060 );
2061 assert!(
2062 result[0].message.contains("Use fenced code blocks"),
2063 "Expected MD046 warning for indented code block"
2064 );
2065 assert!(result[0].line >= 7, "Warning should be on the indented code block line");
2066 }
2067
2068 #[test]
2069 fn test_footnote_terminated_by_structural_elements() {
2070 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2072 let content = r#"[^1]: Footnote content.
2073
2074 More content.
2075
2076## Heading terminates footnote
2077
2078 This indented content should be flagged.
2079
2080---
2081
2082 This should also be flagged (after horizontal rule)."#;
2083
2084 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2085 let result = rule.check(&ctx).unwrap();
2086
2087 assert_eq!(
2089 result.len(),
2090 2,
2091 "Both indented blocks after termination should be flagged"
2092 );
2093 }
2094
2095 #[test]
2096 fn test_footnote_with_code_block_inside() {
2097 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2100 let content = r#"Text[^1].
2101
2102[^1]: Footnote with code:
2103
2104 ```python
2105 def hello():
2106 print("world")
2107 ```
2108
2109 More footnote text after code."#;
2110
2111 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2112 let result = rule.check(&ctx).unwrap();
2113
2114 assert_eq!(result.len(), 0, "Fenced code blocks within footnotes should be allowed");
2116 }
2117
2118 #[test]
2119 fn test_footnote_with_8_space_indented_code() {
2120 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2123 let content = r#"Text[^1].
2124
2125[^1]: Footnote with nested code.
2126
2127 code block
2128 more code"#;
2129
2130 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2131 let result = rule.check(&ctx).unwrap();
2132
2133 assert_eq!(
2135 result.len(),
2136 0,
2137 "8-space indented code within footnotes represents nested code blocks"
2138 );
2139 }
2140
2141 #[test]
2142 fn test_multiple_footnotes() {
2143 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2146 let content = r#"Text[^1] and more[^2].
2147
2148[^1]: First footnote.
2149
2150 Continuation of first.
2151
2152[^2]: Second footnote starts here, ending the first.
2153
2154 Continuation of second."#;
2155
2156 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2157 let result = rule.check(&ctx).unwrap();
2158
2159 assert_eq!(
2161 result.len(),
2162 0,
2163 "Multiple footnotes should each maintain their continuation context"
2164 );
2165 }
2166
2167 #[test]
2168 fn test_list_item_ends_footnote_context() {
2169 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2171 let content = r#"[^1]: Footnote.
2172
2173 Content in footnote.
2174
2175- List item starts here (ends footnote context).
2176
2177 This indented content is part of the list, not the footnote."#;
2178
2179 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2180 let result = rule.check(&ctx).unwrap();
2181
2182 assert_eq!(
2184 result.len(),
2185 0,
2186 "List items should end footnote context and start their own"
2187 );
2188 }
2189
2190 #[test]
2191 fn test_footnote_vs_actual_indented_code() {
2192 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2195 let content = r#"# Heading
2196
2197Text with footnote[^1].
2198
2199[^1]: Footnote content.
2200
2201 Part of footnote (should not be flagged).
2202
2203Regular paragraph ends footnote context.
2204
2205 This is actual indented code (MUST be flagged)
2206 Should be detected as code block"#;
2207
2208 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2209 let result = rule.check(&ctx).unwrap();
2210
2211 assert_eq!(
2213 result.len(),
2214 1,
2215 "Must still detect indented code blocks outside footnotes"
2216 );
2217 assert!(
2218 result[0].message.contains("Use fenced code blocks"),
2219 "Expected MD046 warning for indented code"
2220 );
2221 assert!(
2222 result[0].line >= 11,
2223 "Warning should be on the actual indented code line"
2224 );
2225 }
2226
2227 #[test]
2228 fn test_spec_compliant_label_characters() {
2229 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2232
2233 assert!(rule.is_footnote_definition("[^test]: text"));
2235 assert!(rule.is_footnote_definition("[^TEST]: text"));
2236 assert!(rule.is_footnote_definition("[^test-name]: text"));
2237 assert!(rule.is_footnote_definition("[^test_name]: text"));
2238 assert!(rule.is_footnote_definition("[^test123]: text"));
2239 assert!(rule.is_footnote_definition("[^123]: text"));
2240 assert!(rule.is_footnote_definition("[^a1b2c3]: text"));
2241
2242 assert!(!rule.is_footnote_definition("[^test.name]: text")); assert!(!rule.is_footnote_definition("[^test name]: text")); assert!(!rule.is_footnote_definition("[^test@name]: text")); assert!(!rule.is_footnote_definition("[^test#name]: text")); assert!(!rule.is_footnote_definition("[^test$name]: text")); assert!(!rule.is_footnote_definition("[^test%name]: text")); }
2250
2251 #[test]
2252 fn test_code_block_inside_html_comment() {
2253 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2256 let content = r#"# Document
2257
2258Some text.
2259
2260<!--
2261Example code block in comment:
2262
2263```typescript
2264console.log("Hello");
2265```
2266
2267More comment text.
2268-->
2269
2270More content."#;
2271
2272 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2273 let result = rule.check(&ctx).unwrap();
2274
2275 assert_eq!(
2276 result.len(),
2277 0,
2278 "Code blocks inside HTML comments should not be flagged as unclosed"
2279 );
2280 }
2281
2282 #[test]
2283 fn test_unclosed_fence_inside_html_comment() {
2284 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2286 let content = r#"# Document
2287
2288<!--
2289Example with intentionally unclosed fence:
2290
2291```
2292code without closing
2293-->
2294
2295More content."#;
2296
2297 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2298 let result = rule.check(&ctx).unwrap();
2299
2300 assert_eq!(
2301 result.len(),
2302 0,
2303 "Unclosed fences inside HTML comments should be ignored"
2304 );
2305 }
2306
2307 #[test]
2308 fn test_multiline_html_comment_with_indented_code() {
2309 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2311 let content = r#"# Document
2312
2313<!--
2314Example:
2315
2316 indented code
2317 more code
2318
2319End of comment.
2320-->
2321
2322Regular text."#;
2323
2324 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2325 let result = rule.check(&ctx).unwrap();
2326
2327 assert_eq!(
2328 result.len(),
2329 0,
2330 "Indented code inside HTML comments should not be flagged"
2331 );
2332 }
2333
2334 #[test]
2335 fn test_code_block_after_html_comment() {
2336 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2338 let content = r#"# Document
2339
2340<!-- comment -->
2341
2342Text before.
2343
2344 indented code should be flagged
2345
2346More text."#;
2347
2348 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2349 let result = rule.check(&ctx).unwrap();
2350
2351 assert_eq!(
2352 result.len(),
2353 1,
2354 "Code blocks after HTML comments should still be detected"
2355 );
2356 assert!(result[0].message.contains("Use fenced code blocks"));
2357 }
2358
2359 #[test]
2360 fn test_four_space_indented_fence_is_not_valid_fence() {
2361 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2364
2365 assert!(rule.is_fenced_code_block_start("```"));
2367 assert!(rule.is_fenced_code_block_start(" ```"));
2368 assert!(rule.is_fenced_code_block_start(" ```"));
2369 assert!(rule.is_fenced_code_block_start(" ```"));
2370
2371 assert!(!rule.is_fenced_code_block_start(" ```"));
2373 assert!(!rule.is_fenced_code_block_start(" ```"));
2374 assert!(!rule.is_fenced_code_block_start(" ```"));
2375
2376 assert!(!rule.is_fenced_code_block_start("\t```"));
2378 }
2379
2380 #[test]
2381 fn test_issue_237_indented_fenced_block_detected_as_indented() {
2382 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2388
2389 let content = r#"## Test
2391
2392 ```js
2393 var foo = "hello";
2394 ```
2395"#;
2396
2397 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2398 let result = rule.check(&ctx).unwrap();
2399
2400 assert_eq!(
2402 result.len(),
2403 1,
2404 "4-space indented fence should be detected as indented code block"
2405 );
2406 assert!(
2407 result[0].message.contains("Use fenced code blocks"),
2408 "Expected 'Use fenced code blocks' message"
2409 );
2410 }
2411
2412 #[test]
2413 fn test_issue_276_indented_code_in_list() {
2414 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2417
2418 let content = r#"1. First item
24192. Second item with code:
2420
2421 # This is a code block in a list
2422 print("Hello, world!")
2423
24244. Third item"#;
2425
2426 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2427 let result = rule.check(&ctx).unwrap();
2428
2429 assert!(
2431 !result.is_empty(),
2432 "Indented code block inside list should be flagged when style=fenced"
2433 );
2434 assert!(
2435 result[0].message.contains("Use fenced code blocks"),
2436 "Expected 'Use fenced code blocks' message"
2437 );
2438 }
2439
2440 #[test]
2441 fn test_three_space_indented_fence_is_valid() {
2442 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2444
2445 let content = r#"## Test
2446
2447 ```js
2448 var foo = "hello";
2449 ```
2450"#;
2451
2452 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2453 let result = rule.check(&ctx).unwrap();
2454
2455 assert_eq!(
2457 result.len(),
2458 0,
2459 "3-space indented fence should be recognized as valid fenced code block"
2460 );
2461 }
2462
2463 #[test]
2464 fn test_indented_style_with_deeply_indented_fenced() {
2465 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2468
2469 let content = r#"Text
2470
2471 ```js
2472 var foo = "hello";
2473 ```
2474
2475More text
2476"#;
2477
2478 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2479 let result = rule.check(&ctx).unwrap();
2480
2481 assert_eq!(
2484 result.len(),
2485 0,
2486 "4-space indented content should be valid when style=indented"
2487 );
2488 }
2489
2490 #[test]
2491 fn test_fix_misplaced_fenced_block() {
2492 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2495
2496 let content = r#"## Test
2497
2498 ```js
2499 var foo = "hello";
2500 ```
2501"#;
2502
2503 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2504 let fixed = rule.fix(&ctx).unwrap();
2505
2506 let expected = r#"## Test
2508
2509```js
2510var foo = "hello";
2511```
2512"#;
2513
2514 assert_eq!(fixed, expected, "Fix should remove indentation, not add more fences");
2515 }
2516
2517 #[test]
2518 fn test_fix_regular_indented_block() {
2519 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2522
2523 let content = r#"Text
2524
2525 var foo = "hello";
2526 console.log(foo);
2527
2528More text
2529"#;
2530
2531 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2532 let fixed = rule.fix(&ctx).unwrap();
2533
2534 assert!(fixed.contains("```\nvar foo"), "Should add opening fence");
2536 assert!(fixed.contains("console.log(foo);\n```"), "Should add closing fence");
2537 }
2538
2539 #[test]
2540 fn test_fix_indented_block_with_fence_like_content() {
2541 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2545
2546 let content = r#"Text
2547
2548 some code
2549 ```not a fence opener
2550 more code
2551"#;
2552
2553 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2554 let fixed = rule.fix(&ctx).unwrap();
2555
2556 assert!(fixed.contains(" some code"), "Unsafe block should be left unchanged");
2558 assert!(!fixed.contains("```\nsome code"), "Should NOT wrap unsafe block");
2559 }
2560
2561 #[test]
2562 fn test_fix_mixed_indented_and_misplaced_blocks() {
2563 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2565
2566 let content = r#"Text
2567
2568 regular indented code
2569
2570More text
2571
2572 ```python
2573 print("hello")
2574 ```
2575"#;
2576
2577 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2578 let fixed = rule.fix(&ctx).unwrap();
2579
2580 assert!(
2582 fixed.contains("```\nregular indented code\n```"),
2583 "First block should be wrapped in fences"
2584 );
2585
2586 assert!(
2588 fixed.contains("\n```python\nprint(\"hello\")\n```"),
2589 "Second block should be dedented, not double-wrapped"
2590 );
2591 assert!(
2593 !fixed.contains("```\n```python"),
2594 "Should not have nested fence openers"
2595 );
2596 }
2597}