1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::utils::calculate_indentation_width_default;
3use crate::utils::mkdocs_admonitions;
4use crate::utils::mkdocs_tabs;
5use crate::utils::range_utils::calculate_line_range;
6use toml;
7
8mod md046_config;
9pub use md046_config::CodeBlockStyle;
10use md046_config::MD046Config;
11
12struct IndentContext<'a> {
14 in_list_context: &'a [bool],
15 in_tab_context: &'a [bool],
16 in_admonition_context: &'a [bool],
17 in_comment_or_html: &'a [bool],
27}
28
29#[derive(Clone)]
35pub struct MD046CodeBlockStyle {
36 config: MD046Config,
37}
38
39impl MD046CodeBlockStyle {
40 pub fn new(style: CodeBlockStyle) -> Self {
41 Self {
42 config: MD046Config { style },
43 }
44 }
45
46 pub fn from_config_struct(config: MD046Config) -> Self {
47 Self { config }
48 }
49
50 fn has_valid_fence_indent(line: &str) -> bool {
55 calculate_indentation_width_default(line) < 4
56 }
57
58 fn is_fenced_code_block_start(&self, line: &str) -> bool {
67 if !Self::has_valid_fence_indent(line) {
68 return false;
69 }
70
71 let trimmed = line.trim_start();
72 trimmed.starts_with("```") || trimmed.starts_with("~~~")
73 }
74
75 fn is_list_item(&self, line: &str) -> bool {
76 let trimmed = line.trim_start();
77 (trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ "))
78 || (trimmed.len() > 2
79 && trimmed.chars().next().unwrap().is_numeric()
80 && (trimmed.contains(". ") || trimmed.contains(") ")))
81 }
82
83 fn is_footnote_definition(&self, line: &str) -> bool {
103 let trimmed = line.trim_start();
104 if !trimmed.starts_with("[^") || trimmed.len() < 5 {
105 return false;
106 }
107
108 if let Some(close_bracket_pos) = trimmed.find("]:")
109 && close_bracket_pos > 2
110 {
111 let label = &trimmed[2..close_bracket_pos];
112
113 if label.trim().is_empty() {
114 return false;
115 }
116
117 if label.contains('\r') {
119 return false;
120 }
121
122 if label.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
124 return true;
125 }
126 }
127
128 false
129 }
130
131 fn precompute_block_continuation_context(&self, lines: &[&str]) -> Vec<bool> {
154 let mut in_continuation_context = vec![false; lines.len()];
155 let mut last_list_item_line: Option<usize> = None;
156 let mut last_footnote_line: Option<usize> = None;
157 let mut blank_line_count = 0;
158
159 for (i, line) in lines.iter().enumerate() {
160 let trimmed = line.trim_start();
161 let indent_len = line.len() - trimmed.len();
162
163 if self.is_list_item(line) {
165 last_list_item_line = Some(i);
166 last_footnote_line = None; blank_line_count = 0;
168 in_continuation_context[i] = true;
169 continue;
170 }
171
172 if self.is_footnote_definition(line) {
174 last_footnote_line = Some(i);
175 last_list_item_line = None; blank_line_count = 0;
177 in_continuation_context[i] = true;
178 continue;
179 }
180
181 if line.trim().is_empty() {
183 if last_list_item_line.is_some() || last_footnote_line.is_some() {
185 blank_line_count += 1;
186 in_continuation_context[i] = true;
187
188 }
192 continue;
193 }
194
195 if indent_len == 0 && !trimmed.is_empty() {
197 if trimmed.starts_with('#') {
201 last_list_item_line = None;
202 last_footnote_line = None;
203 blank_line_count = 0;
204 continue;
205 }
206
207 if trimmed.starts_with("---") || trimmed.starts_with("***") {
209 last_list_item_line = None;
210 last_footnote_line = None;
211 blank_line_count = 0;
212 continue;
213 }
214
215 if let Some(list_line) = last_list_item_line
218 && (i - list_line > 5 || blank_line_count > 1)
219 {
220 last_list_item_line = None;
221 }
222
223 if last_footnote_line.is_some() {
225 last_footnote_line = None;
226 }
227
228 blank_line_count = 0;
229
230 if last_list_item_line.is_none() && last_footnote_line.is_some() {
232 last_footnote_line = None;
233 }
234 continue;
235 }
236
237 if indent_len > 0 && (last_list_item_line.is_some() || last_footnote_line.is_some()) {
239 in_continuation_context[i] = true;
240 blank_line_count = 0;
241 }
242 }
243
244 in_continuation_context
245 }
246
247 fn is_indented_code_block_with_context(
249 &self,
250 lines: &[&str],
251 i: usize,
252 is_mkdocs: bool,
253 ctx: &IndentContext,
254 ) -> bool {
255 if i >= lines.len() {
256 return false;
257 }
258
259 let line = lines[i];
260
261 let indent = calculate_indentation_width_default(line);
263 if indent < 4 {
264 return false;
265 }
266
267 if ctx.in_list_context[i] {
269 return false;
270 }
271
272 if is_mkdocs && ctx.in_tab_context[i] {
274 return false;
275 }
276
277 if is_mkdocs && ctx.in_admonition_context[i] {
280 return false;
281 }
282
283 if ctx.in_comment_or_html.get(i).copied().unwrap_or(false) {
289 return false;
290 }
291
292 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
295 let prev_is_indented_code = i > 0
296 && calculate_indentation_width_default(lines[i - 1]) >= 4
297 && !ctx.in_list_context[i - 1]
298 && !(is_mkdocs && ctx.in_tab_context[i - 1])
299 && !(is_mkdocs && ctx.in_admonition_context[i - 1])
300 && !ctx.in_comment_or_html.get(i - 1).copied().unwrap_or(false);
301
302 if !has_blank_line_before && !prev_is_indented_code {
305 return false;
306 }
307
308 true
309 }
310
311 fn precompute_comment_or_html_context(ctx: &crate::lint_context::LintContext, line_count: usize) -> Vec<bool> {
320 (0..line_count)
321 .map(|i| {
322 ctx.line_info(i + 1).is_some_and(|info| {
323 info.in_html_comment
324 || info.in_mdx_comment
325 || info.in_html_block
326 || info.in_jsx_block
327 || info.in_mkdocstrings
328 || info.in_footnote_definition
329 || info.blockquote.is_some()
330 })
331 })
332 .collect()
333 }
334
335 fn precompute_mkdocs_tab_context(&self, lines: &[&str]) -> Vec<bool> {
337 let mut in_tab_context = vec![false; lines.len()];
338 let mut current_tab_indent: Option<usize> = None;
339
340 for (i, line) in lines.iter().enumerate() {
341 if mkdocs_tabs::is_tab_marker(line) {
343 let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
344 current_tab_indent = Some(tab_indent);
345 in_tab_context[i] = true;
346 continue;
347 }
348
349 if let Some(tab_indent) = current_tab_indent {
351 if mkdocs_tabs::is_tab_content(line, tab_indent) {
352 in_tab_context[i] = true;
353 } else if !line.trim().is_empty() && calculate_indentation_width_default(line) < 4 {
354 current_tab_indent = None;
356 } else {
357 in_tab_context[i] = true;
359 }
360 }
361 }
362
363 in_tab_context
364 }
365
366 fn precompute_mkdocs_admonition_context(&self, lines: &[&str]) -> Vec<bool> {
375 let mut in_admonition_context = vec![false; lines.len()];
376 let mut admonition_stack: Vec<usize> = Vec::new();
378
379 for (i, line) in lines.iter().enumerate() {
380 let line_indent = calculate_indentation_width_default(line);
381
382 if mkdocs_admonitions::is_admonition_start(line) {
384 let adm_indent = mkdocs_admonitions::get_admonition_indent(line).unwrap_or(0);
385
386 while let Some(&top_indent) = admonition_stack.last() {
388 if adm_indent <= top_indent {
390 admonition_stack.pop();
391 } else {
392 break;
393 }
394 }
395
396 admonition_stack.push(adm_indent);
398 in_admonition_context[i] = true;
399 continue;
400 }
401
402 if line.trim().is_empty() {
404 if !admonition_stack.is_empty() {
405 in_admonition_context[i] = true;
406 }
407 continue;
408 }
409
410 while let Some(&top_indent) = admonition_stack.last() {
413 if line_indent >= top_indent + 4 {
415 break;
417 } else {
418 admonition_stack.pop();
420 }
421 }
422
423 if !admonition_stack.is_empty() {
425 in_admonition_context[i] = true;
426 }
427 }
428
429 in_admonition_context
430 }
431
432 fn categorize_indented_blocks(
444 &self,
445 lines: &[&str],
446 is_mkdocs: bool,
447 ictx: &IndentContext<'_>,
448 ) -> (Vec<bool>, Vec<bool>) {
449 let mut is_misplaced = vec![false; lines.len()];
450 let mut contains_fences = vec![false; lines.len()];
451
452 let mut i = 0;
454 while i < lines.len() {
455 if !self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx) {
457 i += 1;
458 continue;
459 }
460
461 let block_start = i;
463 let mut block_end = i;
464
465 while block_end < lines.len() && self.is_indented_code_block_with_context(lines, block_end, is_mkdocs, ictx)
466 {
467 block_end += 1;
468 }
469
470 if block_end > block_start {
472 let first_line = lines[block_start].trim_start();
473 let last_line = lines[block_end - 1].trim_start();
474
475 let is_backtick_fence = first_line.starts_with("```");
477 let is_tilde_fence = first_line.starts_with("~~~");
478
479 if is_backtick_fence || is_tilde_fence {
480 let fence_char = if is_backtick_fence { '`' } else { '~' };
481 let opener_len = first_line.chars().take_while(|&c| c == fence_char).count();
482
483 let closer_fence_len = last_line.chars().take_while(|&c| c == fence_char).count();
485 let after_closer = &last_line[closer_fence_len..];
486
487 if closer_fence_len >= opener_len && after_closer.trim().is_empty() {
488 is_misplaced[block_start..block_end].fill(true);
490 } else {
491 contains_fences[block_start..block_end].fill(true);
493 }
494 } else {
495 let has_fence_markers = (block_start..block_end).any(|j| {
498 let trimmed = lines[j].trim_start();
499 trimmed.starts_with("```") || trimmed.starts_with("~~~")
500 });
501
502 if has_fence_markers {
503 contains_fences[block_start..block_end].fill(true);
504 }
505 }
506 }
507
508 i = block_end;
509 }
510
511 (is_misplaced, contains_fences)
512 }
513
514 fn check_unclosed_code_blocks(&self, ctx: &crate::lint_context::LintContext) -> Vec<LintWarning> {
515 let mut warnings = Vec::new();
516 let lines = ctx.raw_lines();
517
518 let has_markdown_doc_block = ctx.code_block_details.iter().any(|d| {
520 if !d.is_fenced {
521 return false;
522 }
523 let lang = d.info_string.to_lowercase();
524 lang.starts_with("markdown") || lang.starts_with("md")
525 });
526
527 if has_markdown_doc_block {
530 return warnings;
531 }
532
533 for detail in &ctx.code_block_details {
534 if !detail.is_fenced {
535 continue;
536 }
537
538 if detail.end != ctx.content.len() {
540 continue;
541 }
542
543 let opening_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
545 Ok(idx) => idx,
546 Err(idx) => idx.saturating_sub(1),
547 };
548
549 let line = lines.get(opening_line_idx).unwrap_or(&"");
551 let trimmed = line.trim();
552 let fence_marker = if let Some(pos) = trimmed.find("```") {
553 let count = trimmed[pos..].chars().take_while(|&c| c == '`').count();
554 "`".repeat(count)
555 } else if let Some(pos) = trimmed.find("~~~") {
556 let count = trimmed[pos..].chars().take_while(|&c| c == '~').count();
557 "~".repeat(count)
558 } else {
559 "```".to_string()
560 };
561
562 let last_non_empty_line = lines.iter().rev().find(|l| !l.trim().is_empty()).unwrap_or(&"");
564 let last_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 == '`' {
568 last_trimmed.starts_with("```") && {
569 let fence_len = last_trimmed.chars().take_while(|&c| c == '`').count();
570 last_trimmed[fence_len..].trim().is_empty()
571 }
572 } else {
573 last_trimmed.starts_with("~~~") && {
574 let fence_len = last_trimmed.chars().take_while(|&c| c == '~').count();
575 last_trimmed[fence_len..].trim().is_empty()
576 }
577 };
578
579 if !has_closing_fence {
580 if ctx
582 .lines
583 .get(opening_line_idx)
584 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
585 {
586 continue;
587 }
588
589 let (start_line, start_col, end_line, end_col) = calculate_line_range(opening_line_idx + 1, line);
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
607 warnings
608 }
609
610 fn detect_style(&self, lines: &[&str], is_mkdocs: bool, ictx: &IndentContext) -> Option<CodeBlockStyle> {
611 if lines.is_empty() {
612 return None;
613 }
614
615 let mut fenced_count = 0;
616 let mut indented_count = 0;
617
618 let mut in_fenced = false;
628 let mut prev_was_indented = false;
629
630 for (i, line) in lines.iter().enumerate() {
631 let in_container = ictx.in_comment_or_html.get(i).copied().unwrap_or(false);
632
633 if self.is_fenced_code_block_start(line) {
634 if in_container {
635 prev_was_indented = false;
638 continue;
639 }
640 if !in_fenced {
641 fenced_count += 1;
643 in_fenced = true;
644 } else {
645 in_fenced = false;
647 }
648 prev_was_indented = false;
649 } else if !in_fenced && self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx) {
650 if !prev_was_indented {
652 indented_count += 1;
653 }
654 prev_was_indented = true;
655 } else {
656 prev_was_indented = false;
657 }
658 }
659
660 if fenced_count == 0 && indented_count == 0 {
661 None
662 } else if fenced_count > 0 && indented_count == 0 {
663 Some(CodeBlockStyle::Fenced)
664 } else if fenced_count == 0 && indented_count > 0 {
665 Some(CodeBlockStyle::Indented)
666 } else if fenced_count >= indented_count {
667 Some(CodeBlockStyle::Fenced)
668 } else {
669 Some(CodeBlockStyle::Indented)
670 }
671 }
672}
673
674impl Rule for MD046CodeBlockStyle {
675 fn name(&self) -> &'static str {
676 "MD046"
677 }
678
679 fn description(&self) -> &'static str {
680 "Code blocks should use a consistent style"
681 }
682
683 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
684 if ctx.content.is_empty() {
686 return Ok(Vec::new());
687 }
688
689 if !ctx.content.contains("```")
691 && !ctx.content.contains("~~~")
692 && !ctx.content.contains(" ")
693 && !ctx.content.contains('\t')
694 {
695 return Ok(Vec::new());
696 }
697
698 let unclosed_warnings = self.check_unclosed_code_blocks(ctx);
700
701 if !unclosed_warnings.is_empty() {
703 return Ok(unclosed_warnings);
704 }
705
706 let lines = ctx.raw_lines();
708 let mut warnings = Vec::new();
709
710 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
711
712 let target_style = match self.config.style {
714 CodeBlockStyle::Consistent => {
715 let in_list_context = self.precompute_block_continuation_context(lines);
716 let in_comment_or_html = Self::precompute_comment_or_html_context(ctx, lines.len());
717 let in_tab_context = if is_mkdocs {
718 self.precompute_mkdocs_tab_context(lines)
719 } else {
720 vec![false; lines.len()]
721 };
722 let in_admonition_context = if is_mkdocs {
723 self.precompute_mkdocs_admonition_context(lines)
724 } else {
725 vec![false; lines.len()]
726 };
727 let ictx = IndentContext {
728 in_list_context: &in_list_context,
729 in_tab_context: &in_tab_context,
730 in_admonition_context: &in_admonition_context,
731 in_comment_or_html: &in_comment_or_html,
732 };
733 self.detect_style(lines, is_mkdocs, &ictx)
734 .unwrap_or(CodeBlockStyle::Fenced)
735 }
736 _ => self.config.style,
737 };
738
739 let mut reported_indented_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
741
742 for detail in &ctx.code_block_details {
743 if detail.start >= ctx.content.len() || detail.end > ctx.content.len() {
744 continue;
745 }
746
747 let start_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
748 Ok(idx) => idx,
749 Err(idx) => idx.saturating_sub(1),
750 };
751
752 if detail.is_fenced {
753 if target_style == CodeBlockStyle::Indented {
754 let line = lines.get(start_line_idx).unwrap_or(&"");
755
756 if ctx
757 .lines
758 .get(start_line_idx)
759 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment || info.in_footnote_definition)
760 {
761 continue;
762 }
763
764 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
765 warnings.push(LintWarning {
766 rule_name: Some(self.name().to_string()),
767 line: start_line,
768 column: start_col,
769 end_line,
770 end_column: end_col,
771 message: "Use indented code blocks".to_string(),
772 severity: Severity::Warning,
773 fix: None,
774 });
775 }
776 } else {
777 if target_style == CodeBlockStyle::Fenced && !reported_indented_lines.contains(&start_line_idx) {
779 let line = lines.get(start_line_idx).unwrap_or(&"");
780
781 if ctx.lines.get(start_line_idx).is_some_and(|info| {
783 info.in_html_comment
784 || info.in_mdx_comment
785 || info.in_html_block
786 || info.in_jsx_block
787 || info.in_mkdocstrings
788 || info.in_footnote_definition
789 || info.blockquote.is_some()
790 }) {
791 continue;
792 }
793
794 if is_mkdocs
796 && ctx
797 .lines
798 .get(start_line_idx)
799 .is_some_and(|info| info.in_admonition || info.in_content_tab)
800 {
801 continue;
802 }
803
804 reported_indented_lines.insert(start_line_idx);
805
806 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
807 warnings.push(LintWarning {
808 rule_name: Some(self.name().to_string()),
809 line: start_line,
810 column: start_col,
811 end_line,
812 end_column: end_col,
813 message: "Use fenced code blocks".to_string(),
814 severity: Severity::Warning,
815 fix: None,
816 });
817 }
818 }
819 }
820
821 warnings.sort_by_key(|w| (w.line, w.column));
823
824 Ok(warnings)
825 }
826
827 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
828 let content = ctx.content;
829 if content.is_empty() {
830 return Ok(String::new());
831 }
832
833 let lines = ctx.raw_lines();
834
835 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
837
838 let in_comment_or_html = Self::precompute_comment_or_html_context(ctx, lines.len());
839
840 let in_list_context = self.precompute_block_continuation_context(lines);
842 let in_tab_context = if is_mkdocs {
843 self.precompute_mkdocs_tab_context(lines)
844 } else {
845 vec![false; lines.len()]
846 };
847 let in_admonition_context = if is_mkdocs {
848 self.precompute_mkdocs_admonition_context(lines)
849 } else {
850 vec![false; lines.len()]
851 };
852
853 let ictx = IndentContext {
854 in_list_context: &in_list_context,
855 in_tab_context: &in_tab_context,
856 in_admonition_context: &in_admonition_context,
857 in_comment_or_html: &in_comment_or_html,
858 };
859
860 let target_style = match self.config.style {
861 CodeBlockStyle::Consistent => self
862 .detect_style(lines, is_mkdocs, &ictx)
863 .unwrap_or(CodeBlockStyle::Fenced),
864 _ => self.config.style,
865 };
866
867 let (misplaced_fence_lines, unsafe_fence_lines) = self.categorize_indented_blocks(lines, is_mkdocs, &ictx);
871
872 let mut result = String::with_capacity(content.len());
873 let mut in_fenced_block = false;
874 let mut fenced_fence_opener: Option<(char, usize)> = None;
878 let mut in_indented_block = false;
879
880 let mut current_block_disabled = false;
882
883 for (i, line) in lines.iter().enumerate() {
884 let line_num = i + 1;
885 let trimmed = line.trim_start();
886
887 if !in_fenced_block
890 && Self::has_valid_fence_indent(line)
891 && (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
892 {
893 current_block_disabled = ctx.inline_config().is_rule_disabled(self.name(), line_num);
895 in_fenced_block = true;
896 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
897 let opener_len = trimmed.chars().take_while(|&c| c == fence_char).count();
898 fenced_fence_opener = Some((fence_char, opener_len));
899
900 if current_block_disabled {
901 result.push_str(line);
903 result.push('\n');
904 } else if target_style == CodeBlockStyle::Indented {
905 in_indented_block = true;
907 } else {
908 result.push_str(line);
910 result.push('\n');
911 }
912 } else if in_fenced_block && fenced_fence_opener.is_some() {
913 let (fence_char, opener_len) = fenced_fence_opener.unwrap();
914 let closer_len = trimmed.chars().take_while(|&c| c == fence_char).count();
917 let after_closer = &trimmed[closer_len..];
918 let is_closer = closer_len >= opener_len && after_closer.trim().is_empty() && closer_len > 0;
919 if is_closer {
920 in_fenced_block = false;
921 fenced_fence_opener = None;
922 in_indented_block = false;
923
924 if current_block_disabled {
925 result.push_str(line);
926 result.push('\n');
927 } else if target_style == CodeBlockStyle::Indented {
928 } else {
930 result.push_str(line);
932 result.push('\n');
933 }
934 current_block_disabled = false;
935 } else if current_block_disabled {
936 result.push_str(line);
938 result.push('\n');
939 } else if target_style == CodeBlockStyle::Indented {
940 result.push_str(" ");
944 result.push_str(line);
945 result.push('\n');
946 } else {
947 result.push_str(line);
949 result.push('\n');
950 }
951 } else if self.is_indented_code_block_with_context(lines, i, is_mkdocs, &ictx) {
952 if ctx.inline_config().is_rule_disabled(self.name(), line_num) {
956 result.push_str(line);
957 result.push('\n');
958 continue;
959 }
960
961 let prev_line_is_indented =
963 i > 0 && self.is_indented_code_block_with_context(lines, i - 1, is_mkdocs, &ictx);
964
965 if target_style == CodeBlockStyle::Fenced {
966 let trimmed_content = line.trim_start();
967
968 if misplaced_fence_lines[i] {
971 result.push_str(trimmed_content);
973 result.push('\n');
974 } else if unsafe_fence_lines[i] {
975 result.push_str(line);
978 result.push('\n');
979 } else if !prev_line_is_indented && !in_indented_block {
980 result.push_str("```\n");
982 result.push_str(trimmed_content);
983 result.push('\n');
984 in_indented_block = true;
985 } else {
986 result.push_str(trimmed_content);
988 result.push('\n');
989 }
990
991 let next_line_is_indented =
993 i < lines.len() - 1 && self.is_indented_code_block_with_context(lines, i + 1, is_mkdocs, &ictx);
994 if !next_line_is_indented
996 && in_indented_block
997 && !misplaced_fence_lines[i]
998 && !unsafe_fence_lines[i]
999 {
1000 result.push_str("```\n");
1001 in_indented_block = false;
1002 }
1003 } else {
1004 result.push_str(line);
1006 result.push('\n');
1007 }
1008 } else {
1009 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1011 result.push_str("```\n");
1012 in_indented_block = false;
1013 }
1014
1015 result.push_str(line);
1016 result.push('\n');
1017 }
1018 }
1019
1020 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1022 result.push_str("```\n");
1023 }
1024
1025 if let Some((fence_char, opener_len)) = fenced_fence_opener
1031 && in_fenced_block
1032 {
1033 let has_unclosed_violation = !self.check_unclosed_code_blocks(ctx).is_empty();
1034 if has_unclosed_violation {
1035 let closer: String = std::iter::repeat_n(fence_char, opener_len).collect();
1036 result.push_str(&closer);
1037 result.push('\n');
1038 }
1039 }
1040
1041 if !content.ends_with('\n') && result.ends_with('\n') {
1043 result.pop();
1044 }
1045
1046 Ok(result)
1047 }
1048
1049 fn category(&self) -> RuleCategory {
1051 RuleCategory::CodeBlock
1052 }
1053
1054 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
1056 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
1059 }
1060
1061 fn as_any(&self) -> &dyn std::any::Any {
1062 self
1063 }
1064
1065 fn default_config_section(&self) -> Option<(String, toml::Value)> {
1066 let json_value = serde_json::to_value(&self.config).ok()?;
1067 Some((
1068 self.name().to_string(),
1069 crate::rule_config_serde::json_to_toml_value(&json_value)?,
1070 ))
1071 }
1072
1073 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1074 where
1075 Self: Sized,
1076 {
1077 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
1078 Box::new(Self::from_config_struct(rule_config))
1079 }
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085 use crate::lint_context::LintContext;
1086
1087 fn detect_style_from_content(rule: &MD046CodeBlockStyle, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
1095 let lines: Vec<&str> = content.lines().collect();
1096 let in_list_context = rule.precompute_block_continuation_context(&lines);
1097 let in_tab_context = if is_mkdocs {
1098 rule.precompute_mkdocs_tab_context(&lines)
1099 } else {
1100 vec![false; lines.len()]
1101 };
1102 let in_admonition_context = if is_mkdocs {
1103 rule.precompute_mkdocs_admonition_context(&lines)
1104 } else {
1105 vec![false; lines.len()]
1106 };
1107 let in_comment_or_html = vec![false; lines.len()];
1108 let ictx = IndentContext {
1109 in_list_context: &in_list_context,
1110 in_tab_context: &in_tab_context,
1111 in_admonition_context: &in_admonition_context,
1112 in_comment_or_html: &in_comment_or_html,
1113 };
1114 rule.detect_style(&lines, is_mkdocs, &ictx)
1115 }
1116
1117 #[test]
1118 fn test_fenced_code_block_detection() {
1119 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1120 assert!(rule.is_fenced_code_block_start("```"));
1121 assert!(rule.is_fenced_code_block_start("```rust"));
1122 assert!(rule.is_fenced_code_block_start("~~~"));
1123 assert!(rule.is_fenced_code_block_start("~~~python"));
1124 assert!(rule.is_fenced_code_block_start(" ```"));
1125 assert!(!rule.is_fenced_code_block_start("``"));
1126 assert!(!rule.is_fenced_code_block_start("~~"));
1127 assert!(!rule.is_fenced_code_block_start("Regular text"));
1128 }
1129
1130 #[test]
1131 fn test_consistent_style_with_fenced_blocks() {
1132 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1133 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
1134 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1135 let result = rule.check(&ctx).unwrap();
1136
1137 assert_eq!(result.len(), 0);
1139 }
1140
1141 #[test]
1142 fn test_consistent_style_with_indented_blocks() {
1143 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1144 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
1145 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1146 let result = rule.check(&ctx).unwrap();
1147
1148 assert_eq!(result.len(), 0);
1150 }
1151
1152 #[test]
1153 fn test_consistent_style_mixed() {
1154 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1155 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
1156 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1157 let result = rule.check(&ctx).unwrap();
1158
1159 assert!(!result.is_empty());
1161 }
1162
1163 #[test]
1164 fn test_fenced_style_with_indented_blocks() {
1165 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1166 let content = "Text\n\n indented code\n more code\n\nMore text";
1167 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1168 let result = rule.check(&ctx).unwrap();
1169
1170 assert!(!result.is_empty());
1172 assert!(result[0].message.contains("Use fenced code blocks"));
1173 }
1174
1175 #[test]
1176 fn test_fenced_style_with_tab_indented_blocks() {
1177 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1178 let content = "Text\n\n\ttab indented code\n\tmore code\n\nMore text";
1179 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1180 let result = rule.check(&ctx).unwrap();
1181
1182 assert!(!result.is_empty());
1184 assert!(result[0].message.contains("Use fenced code blocks"));
1185 }
1186
1187 #[test]
1188 fn test_fenced_style_with_mixed_whitespace_indented_blocks() {
1189 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1190 let content = "Text\n\n \tmixed indent code\n \tmore code\n\nMore text";
1192 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1193 let result = rule.check(&ctx).unwrap();
1194
1195 assert!(
1197 !result.is_empty(),
1198 "Mixed whitespace (2 spaces + tab) should be detected as indented code"
1199 );
1200 assert!(result[0].message.contains("Use fenced code blocks"));
1201 }
1202
1203 #[test]
1204 fn test_fenced_style_with_one_space_tab_indent() {
1205 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1206 let content = "Text\n\n \ttab after one space\n \tmore code\n\nMore text";
1208 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1209 let result = rule.check(&ctx).unwrap();
1210
1211 assert!(!result.is_empty(), "1 space + tab should be detected as indented code");
1212 assert!(result[0].message.contains("Use fenced code blocks"));
1213 }
1214
1215 #[test]
1216 fn test_indented_style_with_fenced_blocks() {
1217 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1218 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
1219 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1220 let result = rule.check(&ctx).unwrap();
1221
1222 assert!(!result.is_empty());
1224 assert!(result[0].message.contains("Use indented code blocks"));
1225 }
1226
1227 #[test]
1228 fn test_unclosed_code_block() {
1229 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1230 let content = "```\ncode without closing fence";
1231 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1232 let result = rule.check(&ctx).unwrap();
1233
1234 assert_eq!(result.len(), 1);
1235 assert!(result[0].message.contains("never closed"));
1236 }
1237
1238 #[test]
1239 fn test_nested_code_blocks() {
1240 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1241 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
1242 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1243 let result = rule.check(&ctx).unwrap();
1244
1245 assert_eq!(result.len(), 0);
1247 }
1248
1249 #[test]
1250 fn test_fix_indented_to_fenced() {
1251 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1252 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
1253 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1254 let fixed = rule.fix(&ctx).unwrap();
1255
1256 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
1257 }
1258
1259 #[test]
1260 fn test_fix_fenced_to_indented() {
1261 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1262 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
1263 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1264 let fixed = rule.fix(&ctx).unwrap();
1265
1266 assert!(fixed.contains(" code line 1\n code line 2"));
1267 assert!(!fixed.contains("```"));
1268 }
1269
1270 #[test]
1271 fn test_fix_fenced_to_indented_preserves_internal_indentation() {
1272 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1275 let content = r#"# Test
1276
1277```html
1278<!doctype html>
1279<html>
1280 <head>
1281 <title>Test</title>
1282 </head>
1283</html>
1284```
1285"#;
1286 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1287 let fixed = rule.fix(&ctx).unwrap();
1288
1289 assert!(
1292 fixed.contains(" <head>"),
1293 "Expected 6 spaces before <head> (4 for code block + 2 original), got:\n{fixed}"
1294 );
1295 assert!(
1296 fixed.contains(" <title>"),
1297 "Expected 8 spaces before <title> (4 for code block + 4 original), got:\n{fixed}"
1298 );
1299 assert!(!fixed.contains("```"), "Fenced markers should be removed");
1300 }
1301
1302 #[test]
1303 fn test_fix_fenced_to_indented_preserves_python_indentation() {
1304 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1306 let content = r#"# Python Example
1307
1308```python
1309def greet(name):
1310 if name:
1311 print(f"Hello, {name}!")
1312 else:
1313 print("Hello, World!")
1314```
1315"#;
1316 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1317 let fixed = rule.fix(&ctx).unwrap();
1318
1319 assert!(
1321 fixed.contains(" def greet(name):"),
1322 "Function def should have 4 spaces (code block indent)"
1323 );
1324 assert!(
1325 fixed.contains(" if name:"),
1326 "if statement should have 8 spaces (4 code + 4 Python)"
1327 );
1328 assert!(
1329 fixed.contains(" print"),
1330 "print should have 12 spaces (4 code + 8 Python)"
1331 );
1332 }
1333
1334 #[test]
1335 fn test_fix_fenced_to_indented_preserves_yaml_indentation() {
1336 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1338 let content = r#"# Config
1339
1340```yaml
1341server:
1342 host: localhost
1343 port: 8080
1344 ssl:
1345 enabled: true
1346 cert: /path/to/cert
1347```
1348"#;
1349 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1350 let fixed = rule.fix(&ctx).unwrap();
1351
1352 assert!(fixed.contains(" server:"), "Root key should have 4 spaces");
1353 assert!(fixed.contains(" host:"), "First level should have 6 spaces");
1354 assert!(fixed.contains(" ssl:"), "ssl key should have 6 spaces");
1355 assert!(fixed.contains(" enabled:"), "Nested ssl should have 8 spaces");
1356 }
1357
1358 #[test]
1359 fn test_fix_fenced_to_indented_preserves_empty_lines() {
1360 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1362 let content = "```\nline1\n\nline2\n```\n";
1363 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1364 let fixed = rule.fix(&ctx).unwrap();
1365
1366 assert!(fixed.contains(" line1"), "line1 should be indented");
1368 assert!(fixed.contains(" line2"), "line2 should be indented");
1369 }
1371
1372 #[test]
1373 fn test_fix_fenced_to_indented_multiple_blocks() {
1374 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1376 let content = r#"# Doc
1377
1378```python
1379def foo():
1380 pass
1381```
1382
1383Text between.
1384
1385```yaml
1386key:
1387 value: 1
1388```
1389"#;
1390 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1391 let fixed = rule.fix(&ctx).unwrap();
1392
1393 assert!(fixed.contains(" def foo():"), "Python def should be indented");
1394 assert!(fixed.contains(" pass"), "Python body should have 8 spaces");
1395 assert!(fixed.contains(" key:"), "YAML root should have 4 spaces");
1396 assert!(fixed.contains(" value:"), "YAML nested should have 6 spaces");
1397 assert!(!fixed.contains("```"), "No fence markers should remain");
1398 }
1399
1400 #[test]
1401 fn test_fix_unclosed_block() {
1402 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1403 let content = "```\ncode without closing";
1404 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1405 let fixed = rule.fix(&ctx).unwrap();
1406
1407 assert!(fixed.ends_with("```"));
1409 }
1410
1411 #[test]
1412 fn test_code_block_in_list() {
1413 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1414 let content = "- List item\n code in list\n more code\n- Next item";
1415 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1416 let result = rule.check(&ctx).unwrap();
1417
1418 assert_eq!(result.len(), 0);
1420 }
1421
1422 #[test]
1423 fn test_detect_style_fenced() {
1424 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1425 let content = "```\ncode\n```";
1426 let style = detect_style_from_content(&rule, content, false);
1427
1428 assert_eq!(style, Some(CodeBlockStyle::Fenced));
1429 }
1430
1431 #[test]
1432 fn test_detect_style_indented() {
1433 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1434 let content = "Text\n\n code\n\nMore";
1435 let style = detect_style_from_content(&rule, content, false);
1436
1437 assert_eq!(style, Some(CodeBlockStyle::Indented));
1438 }
1439
1440 #[test]
1441 fn test_detect_style_none() {
1442 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1443 let content = "No code blocks here";
1444 let style = detect_style_from_content(&rule, content, false);
1445
1446 assert_eq!(style, None);
1447 }
1448
1449 #[test]
1450 fn test_tilde_fence() {
1451 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1452 let content = "~~~\ncode\n~~~";
1453 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1454 let result = rule.check(&ctx).unwrap();
1455
1456 assert_eq!(result.len(), 0);
1458 }
1459
1460 #[test]
1461 fn test_language_specification() {
1462 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1463 let content = "```rust\nfn main() {}\n```";
1464 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1465 let result = rule.check(&ctx).unwrap();
1466
1467 assert_eq!(result.len(), 0);
1468 }
1469
1470 #[test]
1471 fn test_empty_content() {
1472 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1473 let content = "";
1474 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1475 let result = rule.check(&ctx).unwrap();
1476
1477 assert_eq!(result.len(), 0);
1478 }
1479
1480 #[test]
1481 fn test_default_config() {
1482 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1483 let (name, _config) = rule.default_config_section().unwrap();
1484 assert_eq!(name, "MD046");
1485 }
1486
1487 #[test]
1488 fn test_markdown_documentation_block() {
1489 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1490 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
1491 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1492 let result = rule.check(&ctx).unwrap();
1493
1494 assert_eq!(result.len(), 0);
1496 }
1497
1498 #[test]
1499 fn test_preserve_trailing_newline() {
1500 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1501 let content = "```\ncode\n```\n";
1502 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1503 let fixed = rule.fix(&ctx).unwrap();
1504
1505 assert_eq!(fixed, content);
1506 }
1507
1508 #[test]
1509 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
1510 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1511 let content = r#"# Document
1512
1513=== "Python"
1514
1515 This is tab content
1516 Not an indented code block
1517
1518 ```python
1519 def hello():
1520 print("Hello")
1521 ```
1522
1523=== "JavaScript"
1524
1525 More tab content here
1526 Also not an indented code block"#;
1527
1528 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1529 let result = rule.check(&ctx).unwrap();
1530
1531 assert_eq!(result.len(), 0);
1533 }
1534
1535 #[test]
1536 fn test_mkdocs_tabs_with_actual_indented_code() {
1537 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1538 let content = r#"# Document
1539
1540=== "Tab 1"
1541
1542 This is tab content
1543
1544Regular text
1545
1546 This is an actual indented code block
1547 Should be flagged"#;
1548
1549 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1550 let result = rule.check(&ctx).unwrap();
1551
1552 assert_eq!(result.len(), 1);
1554 assert!(result[0].message.contains("Use fenced code blocks"));
1555 }
1556
1557 #[test]
1558 fn test_mkdocs_tabs_detect_style() {
1559 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1560 let content = r#"=== "Tab 1"
1561
1562 Content in tab
1563 More content
1564
1565=== "Tab 2"
1566
1567 Content in second tab"#;
1568
1569 let style = detect_style_from_content(&rule, content, true);
1571 assert_eq!(style, None); let style = detect_style_from_content(&rule, content, false);
1575 assert_eq!(style, Some(CodeBlockStyle::Indented));
1576 }
1577
1578 #[test]
1579 fn test_mkdocs_nested_tabs() {
1580 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1581 let content = r#"# Document
1582
1583=== "Outer Tab"
1584
1585 Some content
1586
1587 === "Nested Tab"
1588
1589 Nested tab content
1590 Should not be flagged"#;
1591
1592 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1593 let result = rule.check(&ctx).unwrap();
1594
1595 assert_eq!(result.len(), 0);
1597 }
1598
1599 #[test]
1600 fn test_mkdocs_admonitions_not_flagged_as_indented_code() {
1601 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1604 let content = r#"# Document
1605
1606!!! note
1607 This is normal admonition content, not a code block.
1608 It spans multiple lines.
1609
1610??? warning "Collapsible Warning"
1611 This is also admonition content.
1612
1613???+ tip "Expanded Tip"
1614 And this one too.
1615
1616Regular text outside admonitions."#;
1617
1618 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1619 let result = rule.check(&ctx).unwrap();
1620
1621 assert_eq!(
1623 result.len(),
1624 0,
1625 "Admonition content in MkDocs mode should not trigger MD046"
1626 );
1627 }
1628
1629 #[test]
1630 fn test_mkdocs_admonition_with_actual_indented_code() {
1631 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1633 let content = r#"# Document
1634
1635!!! note
1636 This is admonition content.
1637
1638Regular text ends the admonition.
1639
1640 This is actual indented code (should be flagged)"#;
1641
1642 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1643 let result = rule.check(&ctx).unwrap();
1644
1645 assert_eq!(result.len(), 1);
1647 assert!(result[0].message.contains("Use fenced code blocks"));
1648 }
1649
1650 #[test]
1651 fn test_admonition_in_standard_mode_flagged() {
1652 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1656 let content = r#"# Document
1657
1658!!! note
1659
1660 This looks like code in standard mode.
1661
1662Regular text."#;
1663
1664 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1666 let result = rule.check(&ctx).unwrap();
1667
1668 assert_eq!(
1670 result.len(),
1671 1,
1672 "Admonition content in Standard mode should be flagged as indented code"
1673 );
1674 }
1675
1676 #[test]
1677 fn test_mkdocs_admonition_with_fenced_code_inside() {
1678 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1680 let content = r#"# Document
1681
1682!!! note "Code Example"
1683 Here's some code:
1684
1685 ```python
1686 def hello():
1687 print("world")
1688 ```
1689
1690 More text after code.
1691
1692Regular text."#;
1693
1694 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1695 let result = rule.check(&ctx).unwrap();
1696
1697 assert_eq!(result.len(), 0, "Fenced code blocks inside admonitions should be valid");
1699 }
1700
1701 #[test]
1702 fn test_mkdocs_nested_admonitions() {
1703 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1705 let content = r#"# Document
1706
1707!!! note "Outer"
1708 Outer content.
1709
1710 !!! warning "Inner"
1711 Inner content.
1712 More inner content.
1713
1714 Back to outer.
1715
1716Regular text."#;
1717
1718 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1719 let result = rule.check(&ctx).unwrap();
1720
1721 assert_eq!(result.len(), 0, "Nested admonitions should not be flagged");
1723 }
1724
1725 #[test]
1726 fn test_mkdocs_admonition_fix_does_not_wrap() {
1727 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1729 let content = r#"!!! note
1730 Content that should stay as admonition content.
1731 Not be wrapped in code fences.
1732"#;
1733
1734 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1735 let fixed = rule.fix(&ctx).unwrap();
1736
1737 assert!(
1739 !fixed.contains("```\n Content"),
1740 "Admonition content should not be wrapped in fences"
1741 );
1742 assert_eq!(fixed, content, "Content should remain unchanged");
1743 }
1744
1745 #[test]
1746 fn test_mkdocs_empty_admonition() {
1747 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1749 let content = r#"!!! note
1750
1751Regular paragraph after empty admonition.
1752
1753 This IS an indented code block (after blank + non-indented line)."#;
1754
1755 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1756 let result = rule.check(&ctx).unwrap();
1757
1758 assert_eq!(result.len(), 1, "Indented code after admonition ends should be flagged");
1760 }
1761
1762 #[test]
1763 fn test_mkdocs_indented_admonition() {
1764 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1766 let content = r#"- List item
1767
1768 !!! note
1769 Indented admonition content.
1770 More content.
1771
1772- Next item"#;
1773
1774 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1775 let result = rule.check(&ctx).unwrap();
1776
1777 assert_eq!(
1779 result.len(),
1780 0,
1781 "Indented admonitions (e.g., in lists) should not be flagged"
1782 );
1783 }
1784
1785 #[test]
1786 fn test_footnote_indented_paragraphs_not_flagged() {
1787 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1788 let content = r#"# Test Document with Footnotes
1789
1790This is some text with a footnote[^1].
1791
1792Here's some code:
1793
1794```bash
1795echo "fenced code block"
1796```
1797
1798More text with another footnote[^2].
1799
1800[^1]: Really interesting footnote text.
1801
1802 Even more interesting second paragraph.
1803
1804[^2]: Another footnote.
1805
1806 With a second paragraph too.
1807
1808 And even a third paragraph!"#;
1809
1810 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1811 let result = rule.check(&ctx).unwrap();
1812
1813 assert_eq!(result.len(), 0);
1815 }
1816
1817 #[test]
1818 fn test_footnote_definition_detection() {
1819 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1820
1821 assert!(rule.is_footnote_definition("[^1]: Footnote text"));
1824 assert!(rule.is_footnote_definition("[^foo]: Footnote text"));
1825 assert!(rule.is_footnote_definition("[^long-name]: Footnote text"));
1826 assert!(rule.is_footnote_definition("[^test_123]: Mixed chars"));
1827 assert!(rule.is_footnote_definition(" [^1]: Indented footnote"));
1828 assert!(rule.is_footnote_definition("[^a]: Minimal valid footnote"));
1829 assert!(rule.is_footnote_definition("[^123]: Numeric label"));
1830 assert!(rule.is_footnote_definition("[^_]: Single underscore"));
1831 assert!(rule.is_footnote_definition("[^-]: Single hyphen"));
1832
1833 assert!(!rule.is_footnote_definition("[^]: No label"));
1835 assert!(!rule.is_footnote_definition("[^ ]: Whitespace only"));
1836 assert!(!rule.is_footnote_definition("[^ ]: Multiple spaces"));
1837 assert!(!rule.is_footnote_definition("[^\t]: Tab only"));
1838
1839 assert!(!rule.is_footnote_definition("[^]]: Extra bracket"));
1841 assert!(!rule.is_footnote_definition("Regular text [^1]:"));
1842 assert!(!rule.is_footnote_definition("[1]: Not a footnote"));
1843 assert!(!rule.is_footnote_definition("[^")); assert!(!rule.is_footnote_definition("[^1:")); assert!(!rule.is_footnote_definition("^1]: Missing opening bracket"));
1846
1847 assert!(!rule.is_footnote_definition("[^test.name]: Period"));
1849 assert!(!rule.is_footnote_definition("[^test name]: Space in label"));
1850 assert!(!rule.is_footnote_definition("[^test@name]: Special char"));
1851 assert!(!rule.is_footnote_definition("[^test/name]: Slash"));
1852 assert!(!rule.is_footnote_definition("[^test\\name]: Backslash"));
1853
1854 assert!(!rule.is_footnote_definition("[^test\r]: Carriage return"));
1857 }
1858
1859 #[test]
1860 fn test_footnote_with_blank_lines() {
1861 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1865 let content = r#"# Document
1866
1867Text with footnote[^1].
1868
1869[^1]: First paragraph.
1870
1871 Second paragraph after blank line.
1872
1873 Third paragraph after another blank line.
1874
1875Regular text at column 0 ends the footnote."#;
1876
1877 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1878 let result = rule.check(&ctx).unwrap();
1879
1880 assert_eq!(
1882 result.len(),
1883 0,
1884 "Indented content within footnotes should not trigger MD046"
1885 );
1886 }
1887
1888 #[test]
1889 fn test_footnote_multiple_consecutive_blank_lines() {
1890 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1893 let content = r#"Text[^1].
1894
1895[^1]: First paragraph.
1896
1897
1898
1899 Content after three blank lines (still part of footnote).
1900
1901Not indented, so footnote ends here."#;
1902
1903 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1904 let result = rule.check(&ctx).unwrap();
1905
1906 assert_eq!(
1908 result.len(),
1909 0,
1910 "Multiple blank lines shouldn't break footnote continuation"
1911 );
1912 }
1913
1914 #[test]
1915 fn test_footnote_terminated_by_non_indented_content() {
1916 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1919 let content = r#"[^1]: Footnote content.
1920
1921 More indented content in footnote.
1922
1923This paragraph is not indented, so footnote ends.
1924
1925 This should be flagged as indented code block."#;
1926
1927 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1928 let result = rule.check(&ctx).unwrap();
1929
1930 assert_eq!(
1932 result.len(),
1933 1,
1934 "Indented code after footnote termination should be flagged"
1935 );
1936 assert!(
1937 result[0].message.contains("Use fenced code blocks"),
1938 "Expected MD046 warning for indented code block"
1939 );
1940 assert!(result[0].line >= 7, "Warning should be on the indented code block line");
1941 }
1942
1943 #[test]
1944 fn test_footnote_terminated_by_structural_elements() {
1945 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1947 let content = r#"[^1]: Footnote content.
1948
1949 More content.
1950
1951## Heading terminates footnote
1952
1953 This indented content should be flagged.
1954
1955---
1956
1957 This should also be flagged (after horizontal rule)."#;
1958
1959 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1960 let result = rule.check(&ctx).unwrap();
1961
1962 assert_eq!(
1964 result.len(),
1965 2,
1966 "Both indented blocks after termination should be flagged"
1967 );
1968 }
1969
1970 #[test]
1971 fn test_footnote_with_code_block_inside() {
1972 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1975 let content = r#"Text[^1].
1976
1977[^1]: Footnote with code:
1978
1979 ```python
1980 def hello():
1981 print("world")
1982 ```
1983
1984 More footnote text after code."#;
1985
1986 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1987 let result = rule.check(&ctx).unwrap();
1988
1989 assert_eq!(result.len(), 0, "Fenced code blocks within footnotes should be allowed");
1991 }
1992
1993 #[test]
1994 fn test_footnote_with_8_space_indented_code() {
1995 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1998 let content = r#"Text[^1].
1999
2000[^1]: Footnote with nested code.
2001
2002 code block
2003 more code"#;
2004
2005 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2006 let result = rule.check(&ctx).unwrap();
2007
2008 assert_eq!(
2010 result.len(),
2011 0,
2012 "8-space indented code within footnotes represents nested code blocks"
2013 );
2014 }
2015
2016 #[test]
2017 fn test_multiple_footnotes() {
2018 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2021 let content = r#"Text[^1] and more[^2].
2022
2023[^1]: First footnote.
2024
2025 Continuation of first.
2026
2027[^2]: Second footnote starts here, ending the first.
2028
2029 Continuation of second."#;
2030
2031 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2032 let result = rule.check(&ctx).unwrap();
2033
2034 assert_eq!(
2036 result.len(),
2037 0,
2038 "Multiple footnotes should each maintain their continuation context"
2039 );
2040 }
2041
2042 #[test]
2043 fn test_list_item_ends_footnote_context() {
2044 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2046 let content = r#"[^1]: Footnote.
2047
2048 Content in footnote.
2049
2050- List item starts here (ends footnote context).
2051
2052 This indented content is part of the list, not the footnote."#;
2053
2054 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2055 let result = rule.check(&ctx).unwrap();
2056
2057 assert_eq!(
2059 result.len(),
2060 0,
2061 "List items should end footnote context and start their own"
2062 );
2063 }
2064
2065 #[test]
2066 fn test_footnote_vs_actual_indented_code() {
2067 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2070 let content = r#"# Heading
2071
2072Text with footnote[^1].
2073
2074[^1]: Footnote content.
2075
2076 Part of footnote (should not be flagged).
2077
2078Regular paragraph ends footnote context.
2079
2080 This is actual indented code (MUST be flagged)
2081 Should be detected as code block"#;
2082
2083 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2084 let result = rule.check(&ctx).unwrap();
2085
2086 assert_eq!(
2088 result.len(),
2089 1,
2090 "Must still detect indented code blocks outside footnotes"
2091 );
2092 assert!(
2093 result[0].message.contains("Use fenced code blocks"),
2094 "Expected MD046 warning for indented code"
2095 );
2096 assert!(
2097 result[0].line >= 11,
2098 "Warning should be on the actual indented code line"
2099 );
2100 }
2101
2102 #[test]
2103 fn test_spec_compliant_label_characters() {
2104 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2107
2108 assert!(rule.is_footnote_definition("[^test]: text"));
2110 assert!(rule.is_footnote_definition("[^TEST]: text"));
2111 assert!(rule.is_footnote_definition("[^test-name]: text"));
2112 assert!(rule.is_footnote_definition("[^test_name]: text"));
2113 assert!(rule.is_footnote_definition("[^test123]: text"));
2114 assert!(rule.is_footnote_definition("[^123]: text"));
2115 assert!(rule.is_footnote_definition("[^a1b2c3]: text"));
2116
2117 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")); }
2125
2126 #[test]
2127 fn test_code_block_inside_html_comment() {
2128 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2131 let content = r#"# Document
2132
2133Some text.
2134
2135<!--
2136Example code block in comment:
2137
2138```typescript
2139console.log("Hello");
2140```
2141
2142More comment text.
2143-->
2144
2145More content."#;
2146
2147 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2148 let result = rule.check(&ctx).unwrap();
2149
2150 assert_eq!(
2151 result.len(),
2152 0,
2153 "Code blocks inside HTML comments should not be flagged as unclosed"
2154 );
2155 }
2156
2157 #[test]
2158 fn test_unclosed_fence_inside_html_comment() {
2159 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2161 let content = r#"# Document
2162
2163<!--
2164Example with intentionally unclosed fence:
2165
2166```
2167code without closing
2168-->
2169
2170More content."#;
2171
2172 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2173 let result = rule.check(&ctx).unwrap();
2174
2175 assert_eq!(
2176 result.len(),
2177 0,
2178 "Unclosed fences inside HTML comments should be ignored"
2179 );
2180 }
2181
2182 #[test]
2183 fn test_multiline_html_comment_with_indented_code() {
2184 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2186 let content = r#"# Document
2187
2188<!--
2189Example:
2190
2191 indented code
2192 more code
2193
2194End of comment.
2195-->
2196
2197Regular text."#;
2198
2199 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2200 let result = rule.check(&ctx).unwrap();
2201
2202 assert_eq!(
2203 result.len(),
2204 0,
2205 "Indented code inside HTML comments should not be flagged"
2206 );
2207 }
2208
2209 #[test]
2210 fn test_code_block_after_html_comment() {
2211 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2213 let content = r#"# Document
2214
2215<!-- comment -->
2216
2217Text before.
2218
2219 indented code should be flagged
2220
2221More text."#;
2222
2223 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2224 let result = rule.check(&ctx).unwrap();
2225
2226 assert_eq!(
2227 result.len(),
2228 1,
2229 "Code blocks after HTML comments should still be detected"
2230 );
2231 assert!(result[0].message.contains("Use fenced code blocks"));
2232 }
2233
2234 #[test]
2235 fn test_consistent_style_indented_html_comment() {
2236 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2242 let content = "# MD046 false-positive reproduction\n\
2243 \n\
2244 <!--\n \
2245 This is just an indented comment, not a code block.\n\
2246 \n \
2247 A second line is required to trigger the false-positive.\n\
2248 \n \
2249 Actually, three lines are required.\n\
2250 -->\n\
2251 \n\
2252 ```md\n\
2253 This should be fine, since it's the only code block and therefore consistent.\n\
2254 ```\n";
2255
2256 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2257 let result = rule.check(&ctx).unwrap();
2258
2259 assert_eq!(
2260 result,
2261 vec![],
2262 "A single fenced block and an indented HTML comment must produce no MD046 warnings",
2263 );
2264 }
2265
2266 #[test]
2267 fn test_consistent_style_indented_html_block() {
2268 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2275 let content = "# Heading\n\
2276 \n\
2277 <div class=\"note\">\n \
2278 line one of indented html content\n \
2279 line two of indented html content\n \
2280 line three of indented html content\n\
2281 </div>\n\
2282 \n\
2283 ```md\n\
2284 real fenced block\n\
2285 ```\n";
2286
2287 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2288 let result = rule.check(&ctx).unwrap();
2289
2290 assert_eq!(
2291 result,
2292 vec![],
2293 "Indented content inside a raw HTML block must not influence MD046 style detection",
2294 );
2295 }
2296
2297 #[test]
2298 fn test_consistent_style_fake_fence_inside_html_comment() {
2299 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2305 let content = "# Title\n\
2306 \n\
2307 <!--\n\
2308 ```\n\
2309 fake fence inside comment\n\
2310 ```\n\
2311 -->\n\
2312 \n \
2313 real indented code block line 1\n \
2314 real indented code block line 2\n";
2315
2316 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2317 let result = rule.check(&ctx).unwrap();
2318
2319 assert_eq!(
2320 result,
2321 vec![],
2322 "Fence markers inside an HTML comment must not influence MD046 style detection",
2323 );
2324 }
2325
2326 #[test]
2327 fn test_consistent_style_indented_footnote_definition() {
2328 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2332 let content = "# Heading\n\
2333 \n\
2334 Reference to a footnote[^note].\n\
2335 \n\
2336 [^note]: First line of the footnote.\n \
2337 Second indented continuation line.\n \
2338 Third indented continuation line.\n \
2339 Fourth indented continuation line.\n\
2340 \n\
2341 ```md\n\
2342 real fenced block\n\
2343 ```\n";
2344
2345 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2346 let result = rule.check(&ctx).unwrap();
2347
2348 assert_eq!(
2349 result,
2350 vec![],
2351 "Footnote-definition continuation content must not influence MD046 style detection",
2352 );
2353 }
2354
2355 #[test]
2356 fn test_consistent_style_indented_blockquote() {
2357 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2362 let content = "# Heading\n\
2363 \n\
2364 > line one of quoted indented content\n\
2365 >\n\
2366 > line two of quoted indented content\n\
2367 >\n\
2368 > line three of quoted indented content\n\
2369 \n\
2370 ```md\n\
2371 real fenced block\n\
2372 ```\n";
2373
2374 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2375 let result = rule.check(&ctx).unwrap();
2376
2377 assert_eq!(
2378 result,
2379 vec![],
2380 "Indented content inside a blockquote must not influence MD046 style detection",
2381 );
2382 }
2383
2384 #[test]
2385 fn test_four_space_indented_fence_is_not_valid_fence() {
2386 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2389
2390 assert!(rule.is_fenced_code_block_start("```"));
2392 assert!(rule.is_fenced_code_block_start(" ```"));
2393 assert!(rule.is_fenced_code_block_start(" ```"));
2394 assert!(rule.is_fenced_code_block_start(" ```"));
2395
2396 assert!(!rule.is_fenced_code_block_start(" ```"));
2398 assert!(!rule.is_fenced_code_block_start(" ```"));
2399 assert!(!rule.is_fenced_code_block_start(" ```"));
2400
2401 assert!(!rule.is_fenced_code_block_start("\t```"));
2403 }
2404
2405 #[test]
2406 fn test_issue_237_indented_fenced_block_detected_as_indented() {
2407 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2413
2414 let content = r#"## Test
2416
2417 ```js
2418 var foo = "hello";
2419 ```
2420"#;
2421
2422 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2423 let result = rule.check(&ctx).unwrap();
2424
2425 assert_eq!(
2427 result.len(),
2428 1,
2429 "4-space indented fence should be detected as indented code block"
2430 );
2431 assert!(
2432 result[0].message.contains("Use fenced code blocks"),
2433 "Expected 'Use fenced code blocks' message"
2434 );
2435 }
2436
2437 #[test]
2438 fn test_issue_276_indented_code_in_list() {
2439 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2442
2443 let content = r#"1. First item
24442. Second item with code:
2445
2446 # This is a code block in a list
2447 print("Hello, world!")
2448
24494. Third item"#;
2450
2451 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2452 let result = rule.check(&ctx).unwrap();
2453
2454 assert!(
2456 !result.is_empty(),
2457 "Indented code block inside list should be flagged when style=fenced"
2458 );
2459 assert!(
2460 result[0].message.contains("Use fenced code blocks"),
2461 "Expected 'Use fenced code blocks' message"
2462 );
2463 }
2464
2465 #[test]
2466 fn test_three_space_indented_fence_is_valid() {
2467 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2469
2470 let content = r#"## Test
2471
2472 ```js
2473 var foo = "hello";
2474 ```
2475"#;
2476
2477 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2478 let result = rule.check(&ctx).unwrap();
2479
2480 assert_eq!(
2482 result.len(),
2483 0,
2484 "3-space indented fence should be recognized as valid fenced code block"
2485 );
2486 }
2487
2488 #[test]
2489 fn test_indented_style_with_deeply_indented_fenced() {
2490 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2493
2494 let content = r#"Text
2495
2496 ```js
2497 var foo = "hello";
2498 ```
2499
2500More text
2501"#;
2502
2503 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2504 let result = rule.check(&ctx).unwrap();
2505
2506 assert_eq!(
2509 result.len(),
2510 0,
2511 "4-space indented content should be valid when style=indented"
2512 );
2513 }
2514
2515 #[test]
2516 fn test_fix_misplaced_fenced_block() {
2517 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2520
2521 let content = r#"## Test
2522
2523 ```js
2524 var foo = "hello";
2525 ```
2526"#;
2527
2528 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2529 let fixed = rule.fix(&ctx).unwrap();
2530
2531 let expected = r#"## Test
2533
2534```js
2535var foo = "hello";
2536```
2537"#;
2538
2539 assert_eq!(fixed, expected, "Fix should remove indentation, not add more fences");
2540 }
2541
2542 #[test]
2543 fn test_fix_regular_indented_block() {
2544 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2547
2548 let content = r#"Text
2549
2550 var foo = "hello";
2551 console.log(foo);
2552
2553More text
2554"#;
2555
2556 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2557 let fixed = rule.fix(&ctx).unwrap();
2558
2559 assert!(fixed.contains("```\nvar foo"), "Should add opening fence");
2561 assert!(fixed.contains("console.log(foo);\n```"), "Should add closing fence");
2562 }
2563
2564 #[test]
2565 fn test_fix_indented_block_with_fence_like_content() {
2566 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2570
2571 let content = r#"Text
2572
2573 some code
2574 ```not a fence opener
2575 more code
2576"#;
2577
2578 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2579 let fixed = rule.fix(&ctx).unwrap();
2580
2581 assert!(fixed.contains(" some code"), "Unsafe block should be left unchanged");
2583 assert!(!fixed.contains("```\nsome code"), "Should NOT wrap unsafe block");
2584 }
2585
2586 #[test]
2587 fn test_fix_mixed_indented_and_misplaced_blocks() {
2588 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2590
2591 let content = r#"Text
2592
2593 regular indented code
2594
2595More text
2596
2597 ```python
2598 print("hello")
2599 ```
2600"#;
2601
2602 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2603 let fixed = rule.fix(&ctx).unwrap();
2604
2605 assert!(
2607 fixed.contains("```\nregular indented code\n```"),
2608 "First block should be wrapped in fences"
2609 );
2610
2611 assert!(
2613 fixed.contains("\n```python\nprint(\"hello\")\n```"),
2614 "Second block should be dedented, not double-wrapped"
2615 );
2616 assert!(
2618 !fixed.contains("```\n```python"),
2619 "Should not have nested fence openers"
2620 );
2621 }
2622}