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 list_item_baseline: &'a [Option<usize>],
37}
38
39#[derive(Clone)]
45pub struct MD046CodeBlockStyle {
46 config: MD046Config,
47}
48
49impl MD046CodeBlockStyle {
50 pub fn new(style: CodeBlockStyle) -> Self {
51 Self {
52 config: MD046Config { style },
53 }
54 }
55
56 pub fn from_config_struct(config: MD046Config) -> Self {
57 Self { config }
58 }
59
60 fn has_valid_fence_indent(line: &str) -> bool {
65 calculate_indentation_width_default(line) < 4
66 }
67
68 fn is_fenced_code_block_start(&self, line: &str) -> bool {
77 if !Self::has_valid_fence_indent(line) {
78 return false;
79 }
80
81 let trimmed = line.trim_start();
82 trimmed.starts_with("```") || trimmed.starts_with("~~~")
83 }
84
85 fn is_list_item(&self, line: &str) -> bool {
86 let trimmed = line.trim_start();
87 (trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ "))
88 || (trimmed.len() > 2
89 && trimmed.chars().next().unwrap().is_numeric()
90 && (trimmed.contains(". ") || trimmed.contains(") ")))
91 }
92
93 fn is_footnote_definition(&self, line: &str) -> bool {
113 let trimmed = line.trim_start();
114 if !trimmed.starts_with("[^") || trimmed.len() < 5 {
115 return false;
116 }
117
118 if let Some(close_bracket_pos) = trimmed.find("]:")
119 && close_bracket_pos > 2
120 {
121 let label = &trimmed[2..close_bracket_pos];
122
123 if label.trim().is_empty() {
124 return false;
125 }
126
127 if label.contains('\r') {
129 return false;
130 }
131
132 if label.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
134 return true;
135 }
136 }
137
138 false
139 }
140
141 fn precompute_block_continuation_context(&self, lines: &[&str]) -> Vec<bool> {
164 let mut in_continuation_context = vec![false; lines.len()];
165 let mut last_list_item_line: Option<usize> = None;
166 let mut last_footnote_line: Option<usize> = None;
167 let mut blank_line_count = 0;
168
169 for (i, line) in lines.iter().enumerate() {
170 let trimmed = line.trim_start();
171 let indent_len = line.len() - trimmed.len();
172
173 if self.is_list_item(line) {
175 last_list_item_line = Some(i);
176 last_footnote_line = None; blank_line_count = 0;
178 in_continuation_context[i] = true;
179 continue;
180 }
181
182 if self.is_footnote_definition(line) {
184 last_footnote_line = Some(i);
185 last_list_item_line = None; blank_line_count = 0;
187 in_continuation_context[i] = true;
188 continue;
189 }
190
191 if line.trim().is_empty() {
193 if last_list_item_line.is_some() || last_footnote_line.is_some() {
195 blank_line_count += 1;
196 in_continuation_context[i] = true;
197
198 }
202 continue;
203 }
204
205 if indent_len == 0 && !trimmed.is_empty() {
207 if trimmed.starts_with('#') {
211 last_list_item_line = None;
212 last_footnote_line = None;
213 blank_line_count = 0;
214 continue;
215 }
216
217 if trimmed.starts_with("---") || trimmed.starts_with("***") {
219 last_list_item_line = None;
220 last_footnote_line = None;
221 blank_line_count = 0;
222 continue;
223 }
224
225 if let Some(list_line) = last_list_item_line
228 && (i - list_line > 5 || blank_line_count > 1)
229 {
230 last_list_item_line = None;
231 }
232
233 if last_footnote_line.is_some() {
235 last_footnote_line = None;
236 }
237
238 blank_line_count = 0;
239
240 if last_list_item_line.is_none() && last_footnote_line.is_some() {
242 last_footnote_line = None;
243 }
244 continue;
245 }
246
247 if indent_len > 0 && (last_list_item_line.is_some() || last_footnote_line.is_some()) {
249 in_continuation_context[i] = true;
250 blank_line_count = 0;
251 }
252 }
253
254 in_continuation_context
255 }
256
257 fn precompute_list_item_baseline(
268 &self,
269 ctx: &crate::lint_context::LintContext,
270 lines: &[&str],
271 ) -> Vec<Option<usize>> {
272 let mut baselines = vec![None; lines.len()];
273 let mut last_baseline: Option<usize> = None;
274 let mut last_list_item_line: Option<usize> = None;
275 let mut blank_line_count = 0usize;
276
277 for (i, line) in lines.iter().enumerate() {
278 let trimmed = line.trim_start();
279 let indent_len = line.len() - trimmed.len();
280
281 if let Some(item) = ctx.line_info(i + 1).and_then(|li| li.list_item.as_ref()) {
283 last_baseline = Some(item.content_column);
284 last_list_item_line = Some(i);
285 blank_line_count = 0;
286 baselines[i] = last_baseline;
287 continue;
288 }
289
290 if line.trim().is_empty() {
292 if last_baseline.is_some() {
293 blank_line_count += 1;
294 baselines[i] = last_baseline;
295 }
296 continue;
297 }
298
299 if indent_len == 0 {
303 if trimmed.starts_with('#') || trimmed.starts_with("---") || trimmed.starts_with("***") {
304 last_baseline = None;
305 last_list_item_line = None;
306 } else if let Some(list_line) = last_list_item_line
307 && (i - list_line > 5 || blank_line_count > 1)
308 {
309 last_baseline = None;
310 last_list_item_line = None;
311 }
312 blank_line_count = 0;
313 continue;
314 }
315
316 if last_baseline.is_some() {
318 baselines[i] = last_baseline;
319 blank_line_count = 0;
320 }
321 }
322
323 baselines
324 }
325
326 fn is_indented_code_block_with_context(
328 &self,
329 lines: &[&str],
330 i: usize,
331 is_mkdocs: bool,
332 ctx: &IndentContext,
333 ) -> bool {
334 if i >= lines.len() {
335 return false;
336 }
337
338 let line = lines[i];
339
340 let indent = calculate_indentation_width_default(line);
342 if indent < 4 {
343 return false;
344 }
345
346 if ctx.in_list_context[i] {
352 let crosses_baseline = ctx
353 .list_item_baseline
354 .get(i)
355 .copied()
356 .flatten()
357 .is_some_and(|base| indent >= base + 4);
358 if !crosses_baseline {
359 return false;
360 }
361 }
362
363 if is_mkdocs && ctx.in_tab_context[i] {
365 return false;
366 }
367
368 if is_mkdocs && ctx.in_admonition_context[i] {
371 return false;
372 }
373
374 if ctx.in_comment_or_html.get(i).copied().unwrap_or(false) {
380 return false;
381 }
382
383 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
388 let prev_is_indented_code = i > 0
389 && {
390 let prev_indent = calculate_indentation_width_default(lines[i - 1]);
391 if prev_indent < 4 {
392 false
393 } else if ctx.in_list_context[i - 1] {
394 ctx.list_item_baseline
395 .get(i - 1)
396 .copied()
397 .flatten()
398 .is_some_and(|base| prev_indent >= base + 4)
399 } else {
400 true
401 }
402 }
403 && !(is_mkdocs && ctx.in_tab_context[i - 1])
404 && !(is_mkdocs && ctx.in_admonition_context[i - 1])
405 && !ctx.in_comment_or_html.get(i - 1).copied().unwrap_or(false);
406
407 if !has_blank_line_before && !prev_is_indented_code {
410 return false;
411 }
412
413 true
414 }
415
416 fn precompute_comment_or_html_context(ctx: &crate::lint_context::LintContext, line_count: usize) -> Vec<bool> {
425 (0..line_count)
426 .map(|i| {
427 ctx.line_info(i + 1).is_some_and(|info| {
428 info.in_html_comment
429 || info.in_mdx_comment
430 || info.in_html_block
431 || info.in_jsx_block
432 || info.in_mkdocstrings
433 || info.in_footnote_definition
434 || info.blockquote.is_some()
435 })
436 })
437 .collect()
438 }
439
440 fn precompute_mkdocs_tab_context(&self, lines: &[&str]) -> Vec<bool> {
442 let mut in_tab_context = vec![false; lines.len()];
443 let mut current_tab_indent: Option<usize> = None;
444
445 for (i, line) in lines.iter().enumerate() {
446 if mkdocs_tabs::is_tab_marker(line) {
448 let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
449 current_tab_indent = Some(tab_indent);
450 in_tab_context[i] = true;
451 continue;
452 }
453
454 if let Some(tab_indent) = current_tab_indent {
456 if mkdocs_tabs::is_tab_content(line, tab_indent) {
457 in_tab_context[i] = true;
458 } else if !line.trim().is_empty() && calculate_indentation_width_default(line) < 4 {
459 current_tab_indent = None;
461 } else {
462 in_tab_context[i] = true;
464 }
465 }
466 }
467
468 in_tab_context
469 }
470
471 fn precompute_mkdocs_admonition_context(&self, lines: &[&str]) -> Vec<bool> {
480 let mut in_admonition_context = vec![false; lines.len()];
481 let mut admonition_stack: Vec<usize> = Vec::new();
483
484 for (i, line) in lines.iter().enumerate() {
485 let line_indent = calculate_indentation_width_default(line);
486
487 if mkdocs_admonitions::is_admonition_start(line) {
489 let adm_indent = mkdocs_admonitions::get_admonition_indent(line).unwrap_or(0);
490
491 while let Some(&top_indent) = admonition_stack.last() {
493 if adm_indent <= top_indent {
495 admonition_stack.pop();
496 } else {
497 break;
498 }
499 }
500
501 admonition_stack.push(adm_indent);
503 in_admonition_context[i] = true;
504 continue;
505 }
506
507 if line.trim().is_empty() {
509 if !admonition_stack.is_empty() {
510 in_admonition_context[i] = true;
511 }
512 continue;
513 }
514
515 while let Some(&top_indent) = admonition_stack.last() {
518 if line_indent >= top_indent + 4 {
520 break;
522 } else {
523 admonition_stack.pop();
525 }
526 }
527
528 if !admonition_stack.is_empty() {
530 in_admonition_context[i] = true;
531 }
532 }
533
534 in_admonition_context
535 }
536
537 fn categorize_indented_blocks(
549 &self,
550 lines: &[&str],
551 is_mkdocs: bool,
552 ictx: &IndentContext<'_>,
553 ) -> (Vec<bool>, Vec<bool>) {
554 let mut is_misplaced = vec![false; lines.len()];
555 let mut contains_fences = vec![false; lines.len()];
556
557 let mut i = 0;
559 while i < lines.len() {
560 if !self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx) {
562 i += 1;
563 continue;
564 }
565
566 let block_start = i;
568 let mut block_end = i;
569
570 while block_end < lines.len() && self.is_indented_code_block_with_context(lines, block_end, is_mkdocs, ictx)
571 {
572 block_end += 1;
573 }
574
575 if block_end > block_start {
577 let first_line = lines[block_start].trim_start();
578 let last_line = lines[block_end - 1].trim_start();
579
580 let is_backtick_fence = first_line.starts_with("```");
582 let is_tilde_fence = first_line.starts_with("~~~");
583
584 if is_backtick_fence || is_tilde_fence {
585 let fence_char = if is_backtick_fence { '`' } else { '~' };
586 let opener_len = first_line.chars().take_while(|&c| c == fence_char).count();
587
588 let closer_fence_len = last_line.chars().take_while(|&c| c == fence_char).count();
590 let after_closer = &last_line[closer_fence_len..];
591
592 if closer_fence_len >= opener_len && after_closer.trim().is_empty() {
593 is_misplaced[block_start..block_end].fill(true);
595 } else {
596 contains_fences[block_start..block_end].fill(true);
598 }
599 } else {
600 let has_fence_markers = (block_start..block_end).any(|j| {
603 let trimmed = lines[j].trim_start();
604 trimmed.starts_with("```") || trimmed.starts_with("~~~")
605 });
606
607 if has_fence_markers {
608 contains_fences[block_start..block_end].fill(true);
609 }
610 }
611 }
612
613 i = block_end;
614 }
615
616 (is_misplaced, contains_fences)
617 }
618
619 fn check_unclosed_code_blocks(&self, ctx: &crate::lint_context::LintContext) -> Vec<LintWarning> {
620 let mut warnings = Vec::new();
621 let lines = ctx.raw_lines();
622
623 let has_markdown_doc_block = ctx.code_block_details.iter().any(|d| {
625 if !d.is_fenced {
626 return false;
627 }
628 let lang = d.info_string.to_lowercase();
629 lang.starts_with("markdown") || lang.starts_with("md")
630 });
631
632 if has_markdown_doc_block {
635 return warnings;
636 }
637
638 for detail in &ctx.code_block_details {
639 if !detail.is_fenced {
640 continue;
641 }
642
643 if detail.end != ctx.content.len() {
645 continue;
646 }
647
648 let opening_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
650 Ok(idx) => idx,
651 Err(idx) => idx.saturating_sub(1),
652 };
653
654 let line = lines.get(opening_line_idx).unwrap_or(&"");
656 let trimmed = line.trim();
657 let fence_marker = if let Some(pos) = trimmed.find("```") {
658 let count = trimmed[pos..].chars().take_while(|&c| c == '`').count();
659 "`".repeat(count)
660 } else if let Some(pos) = trimmed.find("~~~") {
661 let count = trimmed[pos..].chars().take_while(|&c| c == '~').count();
662 "~".repeat(count)
663 } else {
664 "```".to_string()
665 };
666
667 let last_non_empty_line = lines.iter().rev().find(|l| !l.trim().is_empty()).unwrap_or(&"");
669 let last_trimmed = last_non_empty_line.trim();
670 let fence_char = fence_marker.chars().next().unwrap_or('`');
671
672 let has_closing_fence = if fence_char == '`' {
673 last_trimmed.starts_with("```") && {
674 let fence_len = last_trimmed.chars().take_while(|&c| c == '`').count();
675 last_trimmed[fence_len..].trim().is_empty()
676 }
677 } else {
678 last_trimmed.starts_with("~~~") && {
679 let fence_len = last_trimmed.chars().take_while(|&c| c == '~').count();
680 last_trimmed[fence_len..].trim().is_empty()
681 }
682 };
683
684 if !has_closing_fence {
685 if ctx
687 .lines
688 .get(opening_line_idx)
689 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
690 {
691 continue;
692 }
693
694 let (start_line, start_col, end_line, end_col) = calculate_line_range(opening_line_idx + 1, line);
695
696 warnings.push(LintWarning {
697 rule_name: Some(self.name().to_string()),
698 line: start_line,
699 column: start_col,
700 end_line,
701 end_column: end_col,
702 message: format!("Code block opened with '{fence_marker}' but never closed"),
703 severity: Severity::Warning,
704 fix: Some(Fix::new(
705 ctx.content.len()..ctx.content.len(),
706 format!("\n{fence_marker}"),
707 )),
708 });
709 }
710 }
711
712 warnings
713 }
714
715 fn detect_style(&self, lines: &[&str], is_mkdocs: bool, ictx: &IndentContext) -> Option<CodeBlockStyle> {
716 if lines.is_empty() {
717 return None;
718 }
719
720 let mut fenced_count = 0;
721 let mut indented_count = 0;
722
723 let mut in_fenced = false;
733 let mut prev_was_indented = false;
734
735 for (i, line) in lines.iter().enumerate() {
736 let in_container = ictx.in_comment_or_html.get(i).copied().unwrap_or(false);
737
738 if self.is_fenced_code_block_start(line) {
739 if in_container {
740 prev_was_indented = false;
743 continue;
744 }
745 if !in_fenced {
746 fenced_count += 1;
748 in_fenced = true;
749 } else {
750 in_fenced = false;
752 }
753 prev_was_indented = false;
754 } else if !in_fenced && self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx) {
755 if !prev_was_indented {
757 indented_count += 1;
758 }
759 prev_was_indented = true;
760 } else {
761 prev_was_indented = false;
762 }
763 }
764
765 if fenced_count == 0 && indented_count == 0 {
766 None
767 } else if fenced_count > 0 && indented_count == 0 {
768 Some(CodeBlockStyle::Fenced)
769 } else if fenced_count == 0 && indented_count > 0 {
770 Some(CodeBlockStyle::Indented)
771 } else if fenced_count >= indented_count {
772 Some(CodeBlockStyle::Fenced)
773 } else {
774 Some(CodeBlockStyle::Indented)
775 }
776 }
777}
778
779impl Rule for MD046CodeBlockStyle {
780 fn name(&self) -> &'static str {
781 "MD046"
782 }
783
784 fn description(&self) -> &'static str {
785 "Code blocks should use a consistent style"
786 }
787
788 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
789 if ctx.content.is_empty() {
791 return Ok(Vec::new());
792 }
793
794 if !ctx.content.contains("```")
796 && !ctx.content.contains("~~~")
797 && !ctx.content.contains(" ")
798 && !ctx.content.contains('\t')
799 {
800 return Ok(Vec::new());
801 }
802
803 let unclosed_warnings = self.check_unclosed_code_blocks(ctx);
805
806 if !unclosed_warnings.is_empty() {
808 return Ok(unclosed_warnings);
809 }
810
811 let lines = ctx.raw_lines();
813 let mut warnings = Vec::new();
814
815 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
816
817 let target_style = match self.config.style {
819 CodeBlockStyle::Consistent => {
820 let in_list_context = self.precompute_block_continuation_context(lines);
821 let list_item_baseline = self.precompute_list_item_baseline(ctx, lines);
822 let in_comment_or_html = Self::precompute_comment_or_html_context(ctx, lines.len());
823 let in_tab_context = if is_mkdocs {
824 self.precompute_mkdocs_tab_context(lines)
825 } else {
826 vec![false; lines.len()]
827 };
828 let in_admonition_context = if is_mkdocs {
829 self.precompute_mkdocs_admonition_context(lines)
830 } else {
831 vec![false; lines.len()]
832 };
833 let ictx = IndentContext {
834 in_list_context: &in_list_context,
835 in_tab_context: &in_tab_context,
836 in_admonition_context: &in_admonition_context,
837 in_comment_or_html: &in_comment_or_html,
838 list_item_baseline: &list_item_baseline,
839 };
840 self.detect_style(lines, is_mkdocs, &ictx)
841 .unwrap_or(CodeBlockStyle::Fenced)
842 }
843 _ => self.config.style,
844 };
845
846 let mut reported_indented_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
848
849 for detail in &ctx.code_block_details {
850 if detail.start >= ctx.content.len() || detail.end > ctx.content.len() {
851 continue;
852 }
853
854 let start_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
855 Ok(idx) => idx,
856 Err(idx) => idx.saturating_sub(1),
857 };
858
859 if detail.is_fenced {
860 if target_style == CodeBlockStyle::Indented {
861 let line = lines.get(start_line_idx).unwrap_or(&"");
862
863 if ctx
864 .lines
865 .get(start_line_idx)
866 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment || info.in_footnote_definition)
867 {
868 continue;
869 }
870
871 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
872 warnings.push(LintWarning {
873 rule_name: Some(self.name().to_string()),
874 line: start_line,
875 column: start_col,
876 end_line,
877 end_column: end_col,
878 message: "Use indented code blocks".to_string(),
879 severity: Severity::Warning,
880 fix: None,
881 });
882 }
883 } else {
884 if target_style == CodeBlockStyle::Fenced && !reported_indented_lines.contains(&start_line_idx) {
886 let line = lines.get(start_line_idx).unwrap_or(&"");
887
888 if ctx.lines.get(start_line_idx).is_some_and(|info| {
890 info.in_html_comment
891 || info.in_mdx_comment
892 || info.in_html_block
893 || info.in_jsx_block
894 || info.in_mkdocstrings
895 || info.in_footnote_definition
896 || info.blockquote.is_some()
897 }) {
898 continue;
899 }
900
901 if is_mkdocs
903 && ctx
904 .lines
905 .get(start_line_idx)
906 .is_some_and(|info| info.in_admonition || info.in_content_tab)
907 {
908 continue;
909 }
910
911 reported_indented_lines.insert(start_line_idx);
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 fenced code blocks".to_string(),
921 severity: Severity::Warning,
922 fix: None,
923 });
924 }
925 }
926 }
927
928 warnings.sort_by_key(|w| (w.line, w.column));
930
931 Ok(warnings)
932 }
933
934 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
935 let content = ctx.content;
936 if content.is_empty() {
937 return Ok(String::new());
938 }
939
940 let lines = ctx.raw_lines();
941
942 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
944
945 let in_comment_or_html = Self::precompute_comment_or_html_context(ctx, lines.len());
946
947 let in_list_context = self.precompute_block_continuation_context(lines);
949 let list_item_baseline = self.precompute_list_item_baseline(ctx, lines);
950 let in_tab_context = if is_mkdocs {
951 self.precompute_mkdocs_tab_context(lines)
952 } else {
953 vec![false; lines.len()]
954 };
955 let in_admonition_context = if is_mkdocs {
956 self.precompute_mkdocs_admonition_context(lines)
957 } else {
958 vec![false; lines.len()]
959 };
960
961 let ictx = IndentContext {
962 in_list_context: &in_list_context,
963 in_tab_context: &in_tab_context,
964 in_admonition_context: &in_admonition_context,
965 in_comment_or_html: &in_comment_or_html,
966 list_item_baseline: &list_item_baseline,
967 };
968
969 let target_style = match self.config.style {
970 CodeBlockStyle::Consistent => self
971 .detect_style(lines, is_mkdocs, &ictx)
972 .unwrap_or(CodeBlockStyle::Fenced),
973 _ => self.config.style,
974 };
975
976 let (misplaced_fence_lines, unsafe_fence_lines) = self.categorize_indented_blocks(lines, is_mkdocs, &ictx);
980
981 let mut result = String::with_capacity(content.len());
982 let mut in_fenced_block = false;
983 let mut fenced_fence_opener: Option<(char, usize)> = None;
987 let mut in_indented_block = false;
988 let mut current_block_fence_indent = String::new();
993
994 let mut current_block_disabled = false;
996
997 for (i, line) in lines.iter().enumerate() {
998 let line_num = i + 1;
999 let trimmed = line.trim_start();
1000
1001 if !in_fenced_block
1004 && Self::has_valid_fence_indent(line)
1005 && (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
1006 {
1007 current_block_disabled = ctx.inline_config().is_rule_disabled(self.name(), line_num);
1009 in_fenced_block = true;
1010 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
1011 let opener_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1012 fenced_fence_opener = Some((fence_char, opener_len));
1013
1014 if current_block_disabled {
1015 result.push_str(line);
1017 result.push('\n');
1018 } else if target_style == CodeBlockStyle::Indented {
1019 in_indented_block = true;
1021 } else {
1022 result.push_str(line);
1024 result.push('\n');
1025 }
1026 } else if in_fenced_block && fenced_fence_opener.is_some() {
1027 let (fence_char, opener_len) = fenced_fence_opener.unwrap();
1028 let closer_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1031 let after_closer = &trimmed[closer_len..];
1032 let is_closer = closer_len >= opener_len && after_closer.trim().is_empty() && closer_len > 0;
1033 if is_closer {
1034 in_fenced_block = false;
1035 fenced_fence_opener = None;
1036 in_indented_block = false;
1037
1038 if current_block_disabled {
1039 result.push_str(line);
1040 result.push('\n');
1041 } else if target_style == CodeBlockStyle::Indented {
1042 } else {
1044 result.push_str(line);
1046 result.push('\n');
1047 }
1048 current_block_disabled = false;
1049 } else if current_block_disabled {
1050 result.push_str(line);
1052 result.push('\n');
1053 } else if target_style == CodeBlockStyle::Indented {
1054 result.push_str(" ");
1058 result.push_str(line);
1059 result.push('\n');
1060 } else {
1061 result.push_str(line);
1063 result.push('\n');
1064 }
1065 } else if self.is_indented_code_block_with_context(lines, i, is_mkdocs, &ictx) {
1066 if ctx.inline_config().is_rule_disabled(self.name(), line_num) {
1070 result.push_str(line);
1071 result.push('\n');
1072 continue;
1073 }
1074
1075 let prev_line_is_indented =
1077 i > 0 && self.is_indented_code_block_with_context(lines, i - 1, is_mkdocs, &ictx);
1078
1079 if target_style == CodeBlockStyle::Fenced {
1080 let baseline = list_item_baseline.get(i).copied().flatten().unwrap_or(0);
1086 let body = line.strip_prefix(" ").unwrap_or(line);
1092
1093 if misplaced_fence_lines[i] {
1096 result.push_str(line.trim_start());
1098 result.push('\n');
1099 } else if unsafe_fence_lines[i] {
1100 result.push_str(line);
1103 result.push('\n');
1104 } else if !prev_line_is_indented && !in_indented_block {
1105 current_block_fence_indent = " ".repeat(baseline);
1107 result.push_str(¤t_block_fence_indent);
1108 result.push_str("```\n");
1109 result.push_str(body);
1110 result.push('\n');
1111 in_indented_block = true;
1112 } else {
1113 result.push_str(body);
1115 result.push('\n');
1116 }
1117
1118 let next_line_is_indented =
1120 i < lines.len() - 1 && self.is_indented_code_block_with_context(lines, i + 1, is_mkdocs, &ictx);
1121 if !next_line_is_indented
1123 && in_indented_block
1124 && !misplaced_fence_lines[i]
1125 && !unsafe_fence_lines[i]
1126 {
1127 result.push_str(¤t_block_fence_indent);
1128 result.push_str("```\n");
1129 in_indented_block = false;
1130 current_block_fence_indent.clear();
1131 }
1132 } else {
1133 result.push_str(line);
1135 result.push('\n');
1136 }
1137 } else {
1138 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1140 result.push_str(¤t_block_fence_indent);
1141 result.push_str("```\n");
1142 in_indented_block = false;
1143 current_block_fence_indent.clear();
1144 }
1145
1146 result.push_str(line);
1147 result.push('\n');
1148 }
1149 }
1150
1151 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1153 result.push_str(¤t_block_fence_indent);
1154 result.push_str("```\n");
1155 }
1156
1157 if let Some((fence_char, opener_len)) = fenced_fence_opener
1163 && in_fenced_block
1164 {
1165 let has_unclosed_violation = !self.check_unclosed_code_blocks(ctx).is_empty();
1166 if has_unclosed_violation {
1167 let closer: String = std::iter::repeat_n(fence_char, opener_len).collect();
1168 result.push_str(&closer);
1169 result.push('\n');
1170 }
1171 }
1172
1173 if !content.ends_with('\n') && result.ends_with('\n') {
1175 result.pop();
1176 }
1177
1178 Ok(result)
1179 }
1180
1181 fn category(&self) -> RuleCategory {
1183 RuleCategory::CodeBlock
1184 }
1185
1186 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
1188 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
1191 }
1192
1193 fn as_any(&self) -> &dyn std::any::Any {
1194 self
1195 }
1196
1197 fn default_config_section(&self) -> Option<(String, toml::Value)> {
1198 let json_value = serde_json::to_value(&self.config).ok()?;
1199 Some((
1200 self.name().to_string(),
1201 crate::rule_config_serde::json_to_toml_value(&json_value)?,
1202 ))
1203 }
1204
1205 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1206 where
1207 Self: Sized,
1208 {
1209 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
1210 Box::new(Self::from_config_struct(rule_config))
1211 }
1212}
1213
1214#[cfg(test)]
1215mod tests {
1216 use super::*;
1217 use crate::lint_context::LintContext;
1218
1219 fn detect_style_from_content(rule: &MD046CodeBlockStyle, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
1227 let lines: Vec<&str> = content.lines().collect();
1228 let in_list_context = rule.precompute_block_continuation_context(&lines);
1229 let in_tab_context = if is_mkdocs {
1230 rule.precompute_mkdocs_tab_context(&lines)
1231 } else {
1232 vec![false; lines.len()]
1233 };
1234 let in_admonition_context = if is_mkdocs {
1235 rule.precompute_mkdocs_admonition_context(&lines)
1236 } else {
1237 vec![false; lines.len()]
1238 };
1239 let in_comment_or_html = vec![false; lines.len()];
1240 let list_item_baseline: Vec<Option<usize>> = vec![None; lines.len()];
1246 let ictx = IndentContext {
1247 in_list_context: &in_list_context,
1248 in_tab_context: &in_tab_context,
1249 in_admonition_context: &in_admonition_context,
1250 in_comment_or_html: &in_comment_or_html,
1251 list_item_baseline: &list_item_baseline,
1252 };
1253 rule.detect_style(&lines, is_mkdocs, &ictx)
1254 }
1255
1256 #[test]
1257 fn test_fenced_code_block_detection() {
1258 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1259 assert!(rule.is_fenced_code_block_start("```"));
1260 assert!(rule.is_fenced_code_block_start("```rust"));
1261 assert!(rule.is_fenced_code_block_start("~~~"));
1262 assert!(rule.is_fenced_code_block_start("~~~python"));
1263 assert!(rule.is_fenced_code_block_start(" ```"));
1264 assert!(!rule.is_fenced_code_block_start("``"));
1265 assert!(!rule.is_fenced_code_block_start("~~"));
1266 assert!(!rule.is_fenced_code_block_start("Regular text"));
1267 }
1268
1269 #[test]
1270 fn test_consistent_style_with_fenced_blocks() {
1271 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1272 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
1273 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1274 let result = rule.check(&ctx).unwrap();
1275
1276 assert_eq!(result.len(), 0);
1278 }
1279
1280 #[test]
1281 fn test_consistent_style_with_indented_blocks() {
1282 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1283 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
1284 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1285 let result = rule.check(&ctx).unwrap();
1286
1287 assert_eq!(result.len(), 0);
1289 }
1290
1291 #[test]
1292 fn test_consistent_style_mixed() {
1293 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1294 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
1295 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1296 let result = rule.check(&ctx).unwrap();
1297
1298 assert!(!result.is_empty());
1300 }
1301
1302 #[test]
1303 fn test_fenced_style_with_indented_blocks() {
1304 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1305 let content = "Text\n\n indented code\n more code\n\nMore text";
1306 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1307 let result = rule.check(&ctx).unwrap();
1308
1309 assert!(!result.is_empty());
1311 assert!(result[0].message.contains("Use fenced code blocks"));
1312 }
1313
1314 #[test]
1315 fn test_fenced_style_with_tab_indented_blocks() {
1316 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1317 let content = "Text\n\n\ttab indented code\n\tmore code\n\nMore text";
1318 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1319 let result = rule.check(&ctx).unwrap();
1320
1321 assert!(!result.is_empty());
1323 assert!(result[0].message.contains("Use fenced code blocks"));
1324 }
1325
1326 #[test]
1327 fn test_fenced_style_with_mixed_whitespace_indented_blocks() {
1328 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1329 let content = "Text\n\n \tmixed indent code\n \tmore code\n\nMore text";
1331 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1332 let result = rule.check(&ctx).unwrap();
1333
1334 assert!(
1336 !result.is_empty(),
1337 "Mixed whitespace (2 spaces + tab) should be detected as indented code"
1338 );
1339 assert!(result[0].message.contains("Use fenced code blocks"));
1340 }
1341
1342 #[test]
1343 fn test_fenced_style_with_one_space_tab_indent() {
1344 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1345 let content = "Text\n\n \ttab after one space\n \tmore code\n\nMore text";
1347 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1348 let result = rule.check(&ctx).unwrap();
1349
1350 assert!(!result.is_empty(), "1 space + tab should be detected as indented code");
1351 assert!(result[0].message.contains("Use fenced code blocks"));
1352 }
1353
1354 #[test]
1355 fn test_indented_style_with_fenced_blocks() {
1356 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1357 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
1358 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1359 let result = rule.check(&ctx).unwrap();
1360
1361 assert!(!result.is_empty());
1363 assert!(result[0].message.contains("Use indented code blocks"));
1364 }
1365
1366 #[test]
1367 fn test_unclosed_code_block() {
1368 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1369 let content = "```\ncode without closing fence";
1370 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1371 let result = rule.check(&ctx).unwrap();
1372
1373 assert_eq!(result.len(), 1);
1374 assert!(result[0].message.contains("never closed"));
1375 }
1376
1377 #[test]
1378 fn test_nested_code_blocks() {
1379 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1380 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
1381 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1382 let result = rule.check(&ctx).unwrap();
1383
1384 assert_eq!(result.len(), 0);
1386 }
1387
1388 #[test]
1389 fn test_fix_indented_to_fenced() {
1390 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1391 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
1392 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1393 let fixed = rule.fix(&ctx).unwrap();
1394
1395 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
1396 }
1397
1398 #[test]
1399 fn test_fix_fenced_to_indented() {
1400 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1401 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
1402 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1403 let fixed = rule.fix(&ctx).unwrap();
1404
1405 assert!(fixed.contains(" code line 1\n code line 2"));
1406 assert!(!fixed.contains("```"));
1407 }
1408
1409 #[test]
1410 fn test_fix_fenced_to_indented_preserves_internal_indentation() {
1411 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1414 let content = r#"# Test
1415
1416```html
1417<!doctype html>
1418<html>
1419 <head>
1420 <title>Test</title>
1421 </head>
1422</html>
1423```
1424"#;
1425 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1426 let fixed = rule.fix(&ctx).unwrap();
1427
1428 assert!(
1431 fixed.contains(" <head>"),
1432 "Expected 6 spaces before <head> (4 for code block + 2 original), got:\n{fixed}"
1433 );
1434 assert!(
1435 fixed.contains(" <title>"),
1436 "Expected 8 spaces before <title> (4 for code block + 4 original), got:\n{fixed}"
1437 );
1438 assert!(!fixed.contains("```"), "Fenced markers should be removed");
1439 }
1440
1441 #[test]
1442 fn test_fix_fenced_to_indented_preserves_python_indentation() {
1443 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1445 let content = r#"# Python Example
1446
1447```python
1448def greet(name):
1449 if name:
1450 print(f"Hello, {name}!")
1451 else:
1452 print("Hello, World!")
1453```
1454"#;
1455 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1456 let fixed = rule.fix(&ctx).unwrap();
1457
1458 assert!(
1460 fixed.contains(" def greet(name):"),
1461 "Function def should have 4 spaces (code block indent)"
1462 );
1463 assert!(
1464 fixed.contains(" if name:"),
1465 "if statement should have 8 spaces (4 code + 4 Python)"
1466 );
1467 assert!(
1468 fixed.contains(" print"),
1469 "print should have 12 spaces (4 code + 8 Python)"
1470 );
1471 }
1472
1473 #[test]
1474 fn test_fix_fenced_to_indented_preserves_yaml_indentation() {
1475 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1477 let content = r#"# Config
1478
1479```yaml
1480server:
1481 host: localhost
1482 port: 8080
1483 ssl:
1484 enabled: true
1485 cert: /path/to/cert
1486```
1487"#;
1488 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1489 let fixed = rule.fix(&ctx).unwrap();
1490
1491 assert!(fixed.contains(" server:"), "Root key should have 4 spaces");
1492 assert!(fixed.contains(" host:"), "First level should have 6 spaces");
1493 assert!(fixed.contains(" ssl:"), "ssl key should have 6 spaces");
1494 assert!(fixed.contains(" enabled:"), "Nested ssl should have 8 spaces");
1495 }
1496
1497 #[test]
1498 fn test_fix_fenced_to_indented_preserves_empty_lines() {
1499 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1501 let content = "```\nline1\n\nline2\n```\n";
1502 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1503 let fixed = rule.fix(&ctx).unwrap();
1504
1505 assert!(fixed.contains(" line1"), "line1 should be indented");
1507 assert!(fixed.contains(" line2"), "line2 should be indented");
1508 }
1510
1511 #[test]
1512 fn test_fix_fenced_to_indented_multiple_blocks() {
1513 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1515 let content = r#"# Doc
1516
1517```python
1518def foo():
1519 pass
1520```
1521
1522Text between.
1523
1524```yaml
1525key:
1526 value: 1
1527```
1528"#;
1529 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1530 let fixed = rule.fix(&ctx).unwrap();
1531
1532 assert!(fixed.contains(" def foo():"), "Python def should be indented");
1533 assert!(fixed.contains(" pass"), "Python body should have 8 spaces");
1534 assert!(fixed.contains(" key:"), "YAML root should have 4 spaces");
1535 assert!(fixed.contains(" value:"), "YAML nested should have 6 spaces");
1536 assert!(!fixed.contains("```"), "No fence markers should remain");
1537 }
1538
1539 #[test]
1540 fn test_fix_unclosed_block() {
1541 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1542 let content = "```\ncode without closing";
1543 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1544 let fixed = rule.fix(&ctx).unwrap();
1545
1546 assert!(fixed.ends_with("```"));
1548 }
1549
1550 #[test]
1551 fn test_code_block_in_list() {
1552 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1553 let content = "- List item\n code in list\n more code\n- Next item";
1554 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1555 let result = rule.check(&ctx).unwrap();
1556
1557 assert_eq!(result.len(), 0);
1559 }
1560
1561 #[test]
1562 fn test_detect_style_fenced() {
1563 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1564 let content = "```\ncode\n```";
1565 let style = detect_style_from_content(&rule, content, false);
1566
1567 assert_eq!(style, Some(CodeBlockStyle::Fenced));
1568 }
1569
1570 #[test]
1571 fn test_detect_style_indented() {
1572 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1573 let content = "Text\n\n code\n\nMore";
1574 let style = detect_style_from_content(&rule, content, false);
1575
1576 assert_eq!(style, Some(CodeBlockStyle::Indented));
1577 }
1578
1579 #[test]
1580 fn test_detect_style_none() {
1581 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1582 let content = "No code blocks here";
1583 let style = detect_style_from_content(&rule, content, false);
1584
1585 assert_eq!(style, None);
1586 }
1587
1588 #[test]
1589 fn test_tilde_fence() {
1590 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1591 let content = "~~~\ncode\n~~~";
1592 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1593 let result = rule.check(&ctx).unwrap();
1594
1595 assert_eq!(result.len(), 0);
1597 }
1598
1599 #[test]
1600 fn test_language_specification() {
1601 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1602 let content = "```rust\nfn main() {}\n```";
1603 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1604 let result = rule.check(&ctx).unwrap();
1605
1606 assert_eq!(result.len(), 0);
1607 }
1608
1609 #[test]
1610 fn test_empty_content() {
1611 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1612 let content = "";
1613 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1614 let result = rule.check(&ctx).unwrap();
1615
1616 assert_eq!(result.len(), 0);
1617 }
1618
1619 #[test]
1620 fn test_default_config() {
1621 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1622 let (name, _config) = rule.default_config_section().unwrap();
1623 assert_eq!(name, "MD046");
1624 }
1625
1626 #[test]
1627 fn test_markdown_documentation_block() {
1628 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1629 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
1630 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1631 let result = rule.check(&ctx).unwrap();
1632
1633 assert_eq!(result.len(), 0);
1635 }
1636
1637 #[test]
1638 fn test_preserve_trailing_newline() {
1639 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1640 let content = "```\ncode\n```\n";
1641 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1642 let fixed = rule.fix(&ctx).unwrap();
1643
1644 assert_eq!(fixed, content);
1645 }
1646
1647 #[test]
1648 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
1649 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1650 let content = r#"# Document
1651
1652=== "Python"
1653
1654 This is tab content
1655 Not an indented code block
1656
1657 ```python
1658 def hello():
1659 print("Hello")
1660 ```
1661
1662=== "JavaScript"
1663
1664 More tab content here
1665 Also not an indented code block"#;
1666
1667 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1668 let result = rule.check(&ctx).unwrap();
1669
1670 assert_eq!(result.len(), 0);
1672 }
1673
1674 #[test]
1675 fn test_mkdocs_tabs_with_actual_indented_code() {
1676 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1677 let content = r#"# Document
1678
1679=== "Tab 1"
1680
1681 This is tab content
1682
1683Regular text
1684
1685 This is an actual indented code block
1686 Should be flagged"#;
1687
1688 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1689 let result = rule.check(&ctx).unwrap();
1690
1691 assert_eq!(result.len(), 1);
1693 assert!(result[0].message.contains("Use fenced code blocks"));
1694 }
1695
1696 #[test]
1697 fn test_mkdocs_tabs_detect_style() {
1698 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1699 let content = r#"=== "Tab 1"
1700
1701 Content in tab
1702 More content
1703
1704=== "Tab 2"
1705
1706 Content in second tab"#;
1707
1708 let style = detect_style_from_content(&rule, content, true);
1710 assert_eq!(style, None); let style = detect_style_from_content(&rule, content, false);
1714 assert_eq!(style, Some(CodeBlockStyle::Indented));
1715 }
1716
1717 #[test]
1718 fn test_mkdocs_nested_tabs() {
1719 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1720 let content = r#"# Document
1721
1722=== "Outer Tab"
1723
1724 Some content
1725
1726 === "Nested Tab"
1727
1728 Nested tab content
1729 Should not be flagged"#;
1730
1731 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1732 let result = rule.check(&ctx).unwrap();
1733
1734 assert_eq!(result.len(), 0);
1736 }
1737
1738 #[test]
1739 fn test_mkdocs_admonitions_not_flagged_as_indented_code() {
1740 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1743 let content = r#"# Document
1744
1745!!! note
1746 This is normal admonition content, not a code block.
1747 It spans multiple lines.
1748
1749??? warning "Collapsible Warning"
1750 This is also admonition content.
1751
1752???+ tip "Expanded Tip"
1753 And this one too.
1754
1755Regular text outside admonitions."#;
1756
1757 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1758 let result = rule.check(&ctx).unwrap();
1759
1760 assert_eq!(
1762 result.len(),
1763 0,
1764 "Admonition content in MkDocs mode should not trigger MD046"
1765 );
1766 }
1767
1768 #[test]
1769 fn test_mkdocs_admonition_with_actual_indented_code() {
1770 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1772 let content = r#"# Document
1773
1774!!! note
1775 This is admonition content.
1776
1777Regular text ends the admonition.
1778
1779 This is actual indented code (should be flagged)"#;
1780
1781 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1782 let result = rule.check(&ctx).unwrap();
1783
1784 assert_eq!(result.len(), 1);
1786 assert!(result[0].message.contains("Use fenced code blocks"));
1787 }
1788
1789 #[test]
1790 fn test_admonition_in_standard_mode_flagged() {
1791 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1795 let content = r#"# Document
1796
1797!!! note
1798
1799 This looks like code in standard mode.
1800
1801Regular text."#;
1802
1803 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1805 let result = rule.check(&ctx).unwrap();
1806
1807 assert_eq!(
1809 result.len(),
1810 1,
1811 "Admonition content in Standard mode should be flagged as indented code"
1812 );
1813 }
1814
1815 #[test]
1816 fn test_mkdocs_admonition_with_fenced_code_inside() {
1817 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1819 let content = r#"# Document
1820
1821!!! note "Code Example"
1822 Here's some code:
1823
1824 ```python
1825 def hello():
1826 print("world")
1827 ```
1828
1829 More text after code.
1830
1831Regular text."#;
1832
1833 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1834 let result = rule.check(&ctx).unwrap();
1835
1836 assert_eq!(result.len(), 0, "Fenced code blocks inside admonitions should be valid");
1838 }
1839
1840 #[test]
1841 fn test_mkdocs_nested_admonitions() {
1842 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1844 let content = r#"# Document
1845
1846!!! note "Outer"
1847 Outer content.
1848
1849 !!! warning "Inner"
1850 Inner content.
1851 More inner content.
1852
1853 Back to outer.
1854
1855Regular text."#;
1856
1857 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1858 let result = rule.check(&ctx).unwrap();
1859
1860 assert_eq!(result.len(), 0, "Nested admonitions should not be flagged");
1862 }
1863
1864 #[test]
1865 fn test_mkdocs_admonition_fix_does_not_wrap() {
1866 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1868 let content = r#"!!! note
1869 Content that should stay as admonition content.
1870 Not be wrapped in code fences.
1871"#;
1872
1873 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1874 let fixed = rule.fix(&ctx).unwrap();
1875
1876 assert!(
1878 !fixed.contains("```\n Content"),
1879 "Admonition content should not be wrapped in fences"
1880 );
1881 assert_eq!(fixed, content, "Content should remain unchanged");
1882 }
1883
1884 #[test]
1885 fn test_mkdocs_empty_admonition() {
1886 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1888 let content = r#"!!! note
1889
1890Regular paragraph after empty admonition.
1891
1892 This IS an indented code block (after blank + non-indented line)."#;
1893
1894 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1895 let result = rule.check(&ctx).unwrap();
1896
1897 assert_eq!(result.len(), 1, "Indented code after admonition ends should be flagged");
1899 }
1900
1901 #[test]
1902 fn test_mkdocs_indented_admonition() {
1903 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1905 let content = r#"- List item
1906
1907 !!! note
1908 Indented admonition content.
1909 More content.
1910
1911- Next item"#;
1912
1913 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1914 let result = rule.check(&ctx).unwrap();
1915
1916 assert_eq!(
1918 result.len(),
1919 0,
1920 "Indented admonitions (e.g., in lists) should not be flagged"
1921 );
1922 }
1923
1924 #[test]
1925 fn test_footnote_indented_paragraphs_not_flagged() {
1926 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1927 let content = r#"# Test Document with Footnotes
1928
1929This is some text with a footnote[^1].
1930
1931Here's some code:
1932
1933```bash
1934echo "fenced code block"
1935```
1936
1937More text with another footnote[^2].
1938
1939[^1]: Really interesting footnote text.
1940
1941 Even more interesting second paragraph.
1942
1943[^2]: Another footnote.
1944
1945 With a second paragraph too.
1946
1947 And even a third paragraph!"#;
1948
1949 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1950 let result = rule.check(&ctx).unwrap();
1951
1952 assert_eq!(result.len(), 0);
1954 }
1955
1956 #[test]
1957 fn test_footnote_definition_detection() {
1958 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1959
1960 assert!(rule.is_footnote_definition("[^1]: Footnote text"));
1963 assert!(rule.is_footnote_definition("[^foo]: Footnote text"));
1964 assert!(rule.is_footnote_definition("[^long-name]: Footnote text"));
1965 assert!(rule.is_footnote_definition("[^test_123]: Mixed chars"));
1966 assert!(rule.is_footnote_definition(" [^1]: Indented footnote"));
1967 assert!(rule.is_footnote_definition("[^a]: Minimal valid footnote"));
1968 assert!(rule.is_footnote_definition("[^123]: Numeric label"));
1969 assert!(rule.is_footnote_definition("[^_]: Single underscore"));
1970 assert!(rule.is_footnote_definition("[^-]: Single hyphen"));
1971
1972 assert!(!rule.is_footnote_definition("[^]: No label"));
1974 assert!(!rule.is_footnote_definition("[^ ]: Whitespace only"));
1975 assert!(!rule.is_footnote_definition("[^ ]: Multiple spaces"));
1976 assert!(!rule.is_footnote_definition("[^\t]: Tab only"));
1977
1978 assert!(!rule.is_footnote_definition("[^]]: Extra bracket"));
1980 assert!(!rule.is_footnote_definition("Regular text [^1]:"));
1981 assert!(!rule.is_footnote_definition("[1]: Not a footnote"));
1982 assert!(!rule.is_footnote_definition("[^")); assert!(!rule.is_footnote_definition("[^1:")); assert!(!rule.is_footnote_definition("^1]: Missing opening bracket"));
1985
1986 assert!(!rule.is_footnote_definition("[^test.name]: Period"));
1988 assert!(!rule.is_footnote_definition("[^test name]: Space in label"));
1989 assert!(!rule.is_footnote_definition("[^test@name]: Special char"));
1990 assert!(!rule.is_footnote_definition("[^test/name]: Slash"));
1991 assert!(!rule.is_footnote_definition("[^test\\name]: Backslash"));
1992
1993 assert!(!rule.is_footnote_definition("[^test\r]: Carriage return"));
1996 }
1997
1998 #[test]
1999 fn test_footnote_with_blank_lines() {
2000 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2004 let content = r#"# Document
2005
2006Text with footnote[^1].
2007
2008[^1]: First paragraph.
2009
2010 Second paragraph after blank line.
2011
2012 Third paragraph after another blank line.
2013
2014Regular text at column 0 ends the footnote."#;
2015
2016 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2017 let result = rule.check(&ctx).unwrap();
2018
2019 assert_eq!(
2021 result.len(),
2022 0,
2023 "Indented content within footnotes should not trigger MD046"
2024 );
2025 }
2026
2027 #[test]
2028 fn test_footnote_multiple_consecutive_blank_lines() {
2029 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2032 let content = r#"Text[^1].
2033
2034[^1]: First paragraph.
2035
2036
2037
2038 Content after three blank lines (still part of footnote).
2039
2040Not indented, so footnote ends here."#;
2041
2042 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2043 let result = rule.check(&ctx).unwrap();
2044
2045 assert_eq!(
2047 result.len(),
2048 0,
2049 "Multiple blank lines shouldn't break footnote continuation"
2050 );
2051 }
2052
2053 #[test]
2054 fn test_footnote_terminated_by_non_indented_content() {
2055 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2058 let content = r#"[^1]: Footnote content.
2059
2060 More indented content in footnote.
2061
2062This paragraph is not indented, so footnote ends.
2063
2064 This should be flagged as indented code block."#;
2065
2066 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2067 let result = rule.check(&ctx).unwrap();
2068
2069 assert_eq!(
2071 result.len(),
2072 1,
2073 "Indented code after footnote termination should be flagged"
2074 );
2075 assert!(
2076 result[0].message.contains("Use fenced code blocks"),
2077 "Expected MD046 warning for indented code block"
2078 );
2079 assert!(result[0].line >= 7, "Warning should be on the indented code block line");
2080 }
2081
2082 #[test]
2083 fn test_footnote_terminated_by_structural_elements() {
2084 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2086 let content = r#"[^1]: Footnote content.
2087
2088 More content.
2089
2090## Heading terminates footnote
2091
2092 This indented content should be flagged.
2093
2094---
2095
2096 This should also be flagged (after horizontal rule)."#;
2097
2098 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2099 let result = rule.check(&ctx).unwrap();
2100
2101 assert_eq!(
2103 result.len(),
2104 2,
2105 "Both indented blocks after termination should be flagged"
2106 );
2107 }
2108
2109 #[test]
2110 fn test_footnote_with_code_block_inside() {
2111 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2114 let content = r#"Text[^1].
2115
2116[^1]: Footnote with code:
2117
2118 ```python
2119 def hello():
2120 print("world")
2121 ```
2122
2123 More footnote text after code."#;
2124
2125 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2126 let result = rule.check(&ctx).unwrap();
2127
2128 assert_eq!(result.len(), 0, "Fenced code blocks within footnotes should be allowed");
2130 }
2131
2132 #[test]
2133 fn test_footnote_with_8_space_indented_code() {
2134 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2137 let content = r#"Text[^1].
2138
2139[^1]: Footnote with nested code.
2140
2141 code block
2142 more code"#;
2143
2144 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2145 let result = rule.check(&ctx).unwrap();
2146
2147 assert_eq!(
2149 result.len(),
2150 0,
2151 "8-space indented code within footnotes represents nested code blocks"
2152 );
2153 }
2154
2155 #[test]
2156 fn test_multiple_footnotes() {
2157 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2160 let content = r#"Text[^1] and more[^2].
2161
2162[^1]: First footnote.
2163
2164 Continuation of first.
2165
2166[^2]: Second footnote starts here, ending the first.
2167
2168 Continuation of second."#;
2169
2170 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2171 let result = rule.check(&ctx).unwrap();
2172
2173 assert_eq!(
2175 result.len(),
2176 0,
2177 "Multiple footnotes should each maintain their continuation context"
2178 );
2179 }
2180
2181 #[test]
2182 fn test_list_item_ends_footnote_context() {
2183 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2185 let content = r#"[^1]: Footnote.
2186
2187 Content in footnote.
2188
2189- List item starts here (ends footnote context).
2190
2191 This indented content is part of the list, not the footnote."#;
2192
2193 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2194 let result = rule.check(&ctx).unwrap();
2195
2196 assert_eq!(
2198 result.len(),
2199 0,
2200 "List items should end footnote context and start their own"
2201 );
2202 }
2203
2204 #[test]
2205 fn test_footnote_vs_actual_indented_code() {
2206 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2209 let content = r#"# Heading
2210
2211Text with footnote[^1].
2212
2213[^1]: Footnote content.
2214
2215 Part of footnote (should not be flagged).
2216
2217Regular paragraph ends footnote context.
2218
2219 This is actual indented code (MUST be flagged)
2220 Should be detected as code block"#;
2221
2222 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2223 let result = rule.check(&ctx).unwrap();
2224
2225 assert_eq!(
2227 result.len(),
2228 1,
2229 "Must still detect indented code blocks outside footnotes"
2230 );
2231 assert!(
2232 result[0].message.contains("Use fenced code blocks"),
2233 "Expected MD046 warning for indented code"
2234 );
2235 assert!(
2236 result[0].line >= 11,
2237 "Warning should be on the actual indented code line"
2238 );
2239 }
2240
2241 #[test]
2242 fn test_spec_compliant_label_characters() {
2243 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2246
2247 assert!(rule.is_footnote_definition("[^test]: text"));
2249 assert!(rule.is_footnote_definition("[^TEST]: text"));
2250 assert!(rule.is_footnote_definition("[^test-name]: text"));
2251 assert!(rule.is_footnote_definition("[^test_name]: text"));
2252 assert!(rule.is_footnote_definition("[^test123]: text"));
2253 assert!(rule.is_footnote_definition("[^123]: text"));
2254 assert!(rule.is_footnote_definition("[^a1b2c3]: text"));
2255
2256 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")); }
2264
2265 #[test]
2266 fn test_code_block_inside_html_comment() {
2267 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2270 let content = r#"# Document
2271
2272Some text.
2273
2274<!--
2275Example code block in comment:
2276
2277```typescript
2278console.log("Hello");
2279```
2280
2281More comment text.
2282-->
2283
2284More content."#;
2285
2286 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2287 let result = rule.check(&ctx).unwrap();
2288
2289 assert_eq!(
2290 result.len(),
2291 0,
2292 "Code blocks inside HTML comments should not be flagged as unclosed"
2293 );
2294 }
2295
2296 #[test]
2297 fn test_unclosed_fence_inside_html_comment() {
2298 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2300 let content = r#"# Document
2301
2302<!--
2303Example with intentionally unclosed fence:
2304
2305```
2306code without closing
2307-->
2308
2309More content."#;
2310
2311 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2312 let result = rule.check(&ctx).unwrap();
2313
2314 assert_eq!(
2315 result.len(),
2316 0,
2317 "Unclosed fences inside HTML comments should be ignored"
2318 );
2319 }
2320
2321 #[test]
2322 fn test_multiline_html_comment_with_indented_code() {
2323 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2325 let content = r#"# Document
2326
2327<!--
2328Example:
2329
2330 indented code
2331 more code
2332
2333End of comment.
2334-->
2335
2336Regular text."#;
2337
2338 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2339 let result = rule.check(&ctx).unwrap();
2340
2341 assert_eq!(
2342 result.len(),
2343 0,
2344 "Indented code inside HTML comments should not be flagged"
2345 );
2346 }
2347
2348 #[test]
2349 fn test_code_block_after_html_comment() {
2350 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2352 let content = r#"# Document
2353
2354<!-- comment -->
2355
2356Text before.
2357
2358 indented code should be flagged
2359
2360More text."#;
2361
2362 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2363 let result = rule.check(&ctx).unwrap();
2364
2365 assert_eq!(
2366 result.len(),
2367 1,
2368 "Code blocks after HTML comments should still be detected"
2369 );
2370 assert!(result[0].message.contains("Use fenced code blocks"));
2371 }
2372
2373 #[test]
2374 fn test_consistent_style_indented_html_comment() {
2375 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2381 let content = "# MD046 false-positive reproduction\n\
2382 \n\
2383 <!--\n \
2384 This is just an indented comment, not a code block.\n\
2385 \n \
2386 A second line is required to trigger the false-positive.\n\
2387 \n \
2388 Actually, three lines are required.\n\
2389 -->\n\
2390 \n\
2391 ```md\n\
2392 This should be fine, since it's the only code block and therefore consistent.\n\
2393 ```\n";
2394
2395 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2396 let result = rule.check(&ctx).unwrap();
2397
2398 assert_eq!(
2399 result,
2400 vec![],
2401 "A single fenced block and an indented HTML comment must produce no MD046 warnings",
2402 );
2403 }
2404
2405 #[test]
2406 fn test_consistent_style_indented_html_block() {
2407 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2414 let content = "# Heading\n\
2415 \n\
2416 <div class=\"note\">\n \
2417 line one of indented html content\n \
2418 line two of indented html content\n \
2419 line three of indented html content\n\
2420 </div>\n\
2421 \n\
2422 ```md\n\
2423 real fenced block\n\
2424 ```\n";
2425
2426 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2427 let result = rule.check(&ctx).unwrap();
2428
2429 assert_eq!(
2430 result,
2431 vec![],
2432 "Indented content inside a raw HTML block must not influence MD046 style detection",
2433 );
2434 }
2435
2436 #[test]
2437 fn test_consistent_style_fake_fence_inside_html_comment() {
2438 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2444 let content = "# Title\n\
2445 \n\
2446 <!--\n\
2447 ```\n\
2448 fake fence inside comment\n\
2449 ```\n\
2450 -->\n\
2451 \n \
2452 real indented code block line 1\n \
2453 real indented code block line 2\n";
2454
2455 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2456 let result = rule.check(&ctx).unwrap();
2457
2458 assert_eq!(
2459 result,
2460 vec![],
2461 "Fence markers inside an HTML comment must not influence MD046 style detection",
2462 );
2463 }
2464
2465 #[test]
2466 fn test_consistent_style_indented_footnote_definition() {
2467 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2471 let content = "# Heading\n\
2472 \n\
2473 Reference to a footnote[^note].\n\
2474 \n\
2475 [^note]: First line of the footnote.\n \
2476 Second indented continuation line.\n \
2477 Third indented continuation line.\n \
2478 Fourth indented continuation line.\n\
2479 \n\
2480 ```md\n\
2481 real fenced block\n\
2482 ```\n";
2483
2484 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2485 let result = rule.check(&ctx).unwrap();
2486
2487 assert_eq!(
2488 result,
2489 vec![],
2490 "Footnote-definition continuation content must not influence MD046 style detection",
2491 );
2492 }
2493
2494 #[test]
2495 fn test_consistent_style_indented_blockquote() {
2496 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2501 let content = "# Heading\n\
2502 \n\
2503 > line one of quoted indented content\n\
2504 >\n\
2505 > line two of quoted indented content\n\
2506 >\n\
2507 > line three of quoted indented content\n\
2508 \n\
2509 ```md\n\
2510 real fenced block\n\
2511 ```\n";
2512
2513 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2514 let result = rule.check(&ctx).unwrap();
2515
2516 assert_eq!(
2517 result,
2518 vec![],
2519 "Indented content inside a blockquote must not influence MD046 style detection",
2520 );
2521 }
2522
2523 #[test]
2524 fn test_consistent_style_genuine_indented_block_detected_as_indented() {
2525 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2530 let content = "# Heading\n\
2531 \n\
2532 Some prose.\n\
2533 \n \
2534 real indented code line 1\n \
2535 real indented code line 2\n";
2536
2537 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2538 let result = rule.check(&ctx).unwrap();
2539
2540 assert_eq!(
2543 result,
2544 vec![],
2545 "A genuine top-level indented block must be detected as Indented style under Consistent",
2546 );
2547 }
2548
2549 #[test]
2550 fn test_consistent_style_skipped_lines_dont_override_real_block() {
2551 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2556 let content = "# Heading\n\
2557 \n\
2558 <!--\n \
2559 skipped indented comment line 1\n \
2560 skipped indented comment line 2\n\
2561 -->\n\
2562 \n\
2563 <!--\n \
2564 second skipped region\n \
2565 also skipped\n\
2566 -->\n\
2567 \n \
2568 real indented code line\n";
2569
2570 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2571 let result = rule.check(&ctx).unwrap();
2572
2573 assert_eq!(
2574 result,
2575 vec![],
2576 "Skipped container lines must not outweigh the single real indented block",
2577 );
2578 }
2579
2580 #[test]
2581 fn test_consistent_style_fenced_wins_over_skipped_indented() {
2582 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2586 let content = "# Heading\n\
2587 \n\
2588 <!--\n \
2589 skipped indented region one\n \
2590 more of region one\n\
2591 -->\n\
2592 \n\
2593 <!--\n \
2594 skipped indented region two\n \
2595 more of region two\n\
2596 -->\n\
2597 \n\
2598 ```md\n\
2599 real fenced block\n\
2600 ```\n";
2601
2602 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2603 let result = rule.check(&ctx).unwrap();
2604
2605 assert_eq!(
2606 result,
2607 vec![],
2608 "Fenced block must win when all indented lines are inside skipped containers",
2609 );
2610 }
2611
2612 #[test]
2613 fn test_four_space_indented_fence_is_not_valid_fence() {
2614 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2617
2618 assert!(rule.is_fenced_code_block_start("```"));
2620 assert!(rule.is_fenced_code_block_start(" ```"));
2621 assert!(rule.is_fenced_code_block_start(" ```"));
2622 assert!(rule.is_fenced_code_block_start(" ```"));
2623
2624 assert!(!rule.is_fenced_code_block_start(" ```"));
2626 assert!(!rule.is_fenced_code_block_start(" ```"));
2627 assert!(!rule.is_fenced_code_block_start(" ```"));
2628
2629 assert!(!rule.is_fenced_code_block_start("\t```"));
2631 }
2632
2633 #[test]
2634 fn test_issue_237_indented_fenced_block_detected_as_indented() {
2635 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2641
2642 let content = r#"## Test
2644
2645 ```js
2646 var foo = "hello";
2647 ```
2648"#;
2649
2650 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2651 let result = rule.check(&ctx).unwrap();
2652
2653 assert_eq!(
2655 result.len(),
2656 1,
2657 "4-space indented fence should be detected as indented code block"
2658 );
2659 assert!(
2660 result[0].message.contains("Use fenced code blocks"),
2661 "Expected 'Use fenced code blocks' message"
2662 );
2663 }
2664
2665 #[test]
2666 fn test_issue_276_indented_code_in_list() {
2667 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2670
2671 let content = r#"1. First item
26722. Second item with code:
2673
2674 # This is a code block in a list
2675 print("Hello, world!")
2676
26774. Third item"#;
2678
2679 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2680 let result = rule.check(&ctx).unwrap();
2681
2682 assert!(
2684 !result.is_empty(),
2685 "Indented code block inside list should be flagged when style=fenced"
2686 );
2687 assert!(
2688 result[0].message.contains("Use fenced code blocks"),
2689 "Expected 'Use fenced code blocks' message"
2690 );
2691 }
2692
2693 #[test]
2694 fn test_three_space_indented_fence_is_valid() {
2695 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2697
2698 let content = r#"## Test
2699
2700 ```js
2701 var foo = "hello";
2702 ```
2703"#;
2704
2705 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2706 let result = rule.check(&ctx).unwrap();
2707
2708 assert_eq!(
2710 result.len(),
2711 0,
2712 "3-space indented fence should be recognized as valid fenced code block"
2713 );
2714 }
2715
2716 #[test]
2717 fn test_indented_style_with_deeply_indented_fenced() {
2718 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2721
2722 let content = r#"Text
2723
2724 ```js
2725 var foo = "hello";
2726 ```
2727
2728More text
2729"#;
2730
2731 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2732 let result = rule.check(&ctx).unwrap();
2733
2734 assert_eq!(
2737 result.len(),
2738 0,
2739 "4-space indented content should be valid when style=indented"
2740 );
2741 }
2742
2743 #[test]
2744 fn test_fix_misplaced_fenced_block() {
2745 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2748
2749 let content = r#"## Test
2750
2751 ```js
2752 var foo = "hello";
2753 ```
2754"#;
2755
2756 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2757 let fixed = rule.fix(&ctx).unwrap();
2758
2759 let expected = r#"## Test
2761
2762```js
2763var foo = "hello";
2764```
2765"#;
2766
2767 assert_eq!(fixed, expected, "Fix should remove indentation, not add more fences");
2768 }
2769
2770 #[test]
2771 fn test_fix_regular_indented_block() {
2772 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2775
2776 let content = r#"Text
2777
2778 var foo = "hello";
2779 console.log(foo);
2780
2781More text
2782"#;
2783
2784 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2785 let fixed = rule.fix(&ctx).unwrap();
2786
2787 assert!(fixed.contains("```\nvar foo"), "Should add opening fence");
2789 assert!(fixed.contains("console.log(foo);\n```"), "Should add closing fence");
2790 }
2791
2792 #[test]
2793 fn test_fix_indented_block_with_fence_like_content() {
2794 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2798
2799 let content = r#"Text
2800
2801 some code
2802 ```not a fence opener
2803 more code
2804"#;
2805
2806 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2807 let fixed = rule.fix(&ctx).unwrap();
2808
2809 assert!(fixed.contains(" some code"), "Unsafe block should be left unchanged");
2811 assert!(!fixed.contains("```\nsome code"), "Should NOT wrap unsafe block");
2812 }
2813
2814 #[test]
2815 fn test_fix_mixed_indented_and_misplaced_blocks() {
2816 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2818
2819 let content = r#"Text
2820
2821 regular indented code
2822
2823More text
2824
2825 ```python
2826 print("hello")
2827 ```
2828"#;
2829
2830 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2831 let fixed = rule.fix(&ctx).unwrap();
2832
2833 assert!(
2835 fixed.contains("```\nregular indented code\n```"),
2836 "First block should be wrapped in fences"
2837 );
2838
2839 assert!(
2841 fixed.contains("\n```python\nprint(\"hello\")\n```"),
2842 "Second block should be dedented, not double-wrapped"
2843 );
2844 assert!(
2846 !fixed.contains("```\n```python"),
2847 "Should not have nested fence openers"
2848 );
2849 }
2850}