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(
716 &self,
717 ctx: &crate::lint_context::LintContext,
718 lines: &[&str],
719 is_mkdocs: bool,
720 ictx: &IndentContext,
721 ) -> Option<CodeBlockStyle> {
722 if lines.is_empty() {
723 return None;
724 }
725
726 let mut fenced_count = 0;
727 let mut indented_count = 0;
728
729 let mut in_fenced = false;
739 let mut prev_was_indented = false;
740
741 for (i, line) in lines.iter().enumerate() {
742 let in_container = ictx.in_comment_or_html.get(i).copied().unwrap_or(false);
743
744 if ctx.flavor.supports_colon_code_fences() && ctx.lines.get(i).is_some_and(|l| l.in_code_block) {
748 prev_was_indented = false;
749 continue;
750 }
751
752 if ctx.flavor.supports_myst_directives() && ctx.lines.get(i).is_some_and(|l| l.in_myst_directive) {
754 prev_was_indented = false;
755 continue;
756 }
757
758 if self.is_fenced_code_block_start(line) {
759 if in_container {
760 prev_was_indented = false;
763 continue;
764 }
765 if !in_fenced {
766 fenced_count += 1;
768 in_fenced = true;
769 } else {
770 in_fenced = false;
772 }
773 prev_was_indented = false;
774 } else if !in_fenced && self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx) {
775 if !prev_was_indented {
777 indented_count += 1;
778 }
779 prev_was_indented = true;
780 } else {
781 prev_was_indented = false;
782 }
783 }
784
785 if fenced_count == 0 && indented_count == 0 {
786 None
787 } else if fenced_count > 0 && indented_count == 0 {
788 Some(CodeBlockStyle::Fenced)
789 } else if fenced_count == 0 && indented_count > 0 {
790 Some(CodeBlockStyle::Indented)
791 } else if fenced_count >= indented_count {
792 Some(CodeBlockStyle::Fenced)
793 } else {
794 Some(CodeBlockStyle::Indented)
795 }
796 }
797}
798
799impl Rule for MD046CodeBlockStyle {
800 fn name(&self) -> &'static str {
801 "MD046"
802 }
803
804 fn description(&self) -> &'static str {
805 "Code blocks should use a consistent style"
806 }
807
808 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
809 if ctx.content.is_empty() {
811 return Ok(Vec::new());
812 }
813
814 if !ctx.content.contains("```")
816 && !ctx.content.contains("~~~")
817 && !ctx.content.contains(" ")
818 && !ctx.content.contains('\t')
819 {
820 return Ok(Vec::new());
821 }
822
823 let unclosed_warnings = self.check_unclosed_code_blocks(ctx);
825
826 if !unclosed_warnings.is_empty() {
828 return Ok(unclosed_warnings);
829 }
830
831 let lines = ctx.raw_lines();
833 let mut warnings = Vec::new();
834
835 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
836
837 let target_style = match self.config.style {
839 CodeBlockStyle::Consistent => {
840 let in_list_context = self.precompute_block_continuation_context(lines);
841 let list_item_baseline = self.precompute_list_item_baseline(ctx, lines);
842 let in_comment_or_html = Self::precompute_comment_or_html_context(ctx, lines.len());
843 let in_tab_context = if is_mkdocs {
844 self.precompute_mkdocs_tab_context(lines)
845 } else {
846 vec![false; lines.len()]
847 };
848 let in_admonition_context = if is_mkdocs {
849 self.precompute_mkdocs_admonition_context(lines)
850 } else {
851 vec![false; lines.len()]
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 list_item_baseline: &list_item_baseline,
859 };
860 self.detect_style(ctx, lines, is_mkdocs, &ictx)
861 .unwrap_or(CodeBlockStyle::Fenced)
862 }
863 _ => self.config.style,
864 };
865
866 let mut reported_indented_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
868
869 for detail in &ctx.code_block_details {
870 if detail.start >= ctx.content.len() || detail.end > ctx.content.len() {
871 continue;
872 }
873
874 let start_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
875 Ok(idx) => idx,
876 Err(idx) => idx.saturating_sub(1),
877 };
878
879 if detail.is_fenced {
880 if target_style == CodeBlockStyle::Indented {
881 let line = lines.get(start_line_idx).unwrap_or(&"");
882
883 if ctx
884 .lines
885 .get(start_line_idx)
886 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment || info.in_footnote_definition)
887 {
888 continue;
889 }
890
891 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
892 warnings.push(LintWarning {
893 rule_name: Some(self.name().to_string()),
894 line: start_line,
895 column: start_col,
896 end_line,
897 end_column: end_col,
898 message: "Use indented code blocks".to_string(),
899 severity: Severity::Warning,
900 fix: None,
901 });
902 }
903 } else {
904 if target_style == CodeBlockStyle::Fenced && !reported_indented_lines.contains(&start_line_idx) {
906 let line = lines.get(start_line_idx).unwrap_or(&"");
907
908 if ctx.lines.get(start_line_idx).is_some_and(|info| {
910 info.in_html_comment
911 || info.in_mdx_comment
912 || info.in_html_block
913 || info.in_jsx_block
914 || info.in_mkdocstrings
915 || info.in_footnote_definition
916 || info.blockquote.is_some()
917 }) {
918 continue;
919 }
920
921 if is_mkdocs
923 && ctx
924 .lines
925 .get(start_line_idx)
926 .is_some_and(|info| info.in_admonition || info.in_content_tab)
927 {
928 continue;
929 }
930
931 reported_indented_lines.insert(start_line_idx);
932
933 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
934 warnings.push(LintWarning {
935 rule_name: Some(self.name().to_string()),
936 line: start_line,
937 column: start_col,
938 end_line,
939 end_column: end_col,
940 message: "Use fenced code blocks".to_string(),
941 severity: Severity::Warning,
942 fix: None,
943 });
944 }
945 }
946 }
947
948 warnings.sort_by_key(|w| (w.line, w.column));
950
951 Ok(warnings)
952 }
953
954 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
955 let content = ctx.content;
956 if content.is_empty() {
957 return Ok(String::new());
958 }
959
960 let lines = ctx.raw_lines();
961
962 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
964
965 let in_comment_or_html = Self::precompute_comment_or_html_context(ctx, lines.len());
966
967 let in_list_context = self.precompute_block_continuation_context(lines);
969 let list_item_baseline = self.precompute_list_item_baseline(ctx, lines);
970 let in_tab_context = if is_mkdocs {
971 self.precompute_mkdocs_tab_context(lines)
972 } else {
973 vec![false; lines.len()]
974 };
975 let in_admonition_context = if is_mkdocs {
976 self.precompute_mkdocs_admonition_context(lines)
977 } else {
978 vec![false; lines.len()]
979 };
980
981 let ictx = IndentContext {
982 in_list_context: &in_list_context,
983 in_tab_context: &in_tab_context,
984 in_admonition_context: &in_admonition_context,
985 in_comment_or_html: &in_comment_or_html,
986 list_item_baseline: &list_item_baseline,
987 };
988
989 let target_style = match self.config.style {
990 CodeBlockStyle::Consistent => self
991 .detect_style(ctx, lines, is_mkdocs, &ictx)
992 .unwrap_or(CodeBlockStyle::Fenced),
993 _ => self.config.style,
994 };
995
996 let (misplaced_fence_lines, unsafe_fence_lines) = self.categorize_indented_blocks(lines, is_mkdocs, &ictx);
1000
1001 let mut result = String::with_capacity(content.len());
1002 let mut in_fenced_block = false;
1003 let mut fenced_fence_opener: Option<(char, usize)> = None;
1007 let mut in_indented_block = false;
1008 let mut current_block_fence_indent = String::new();
1013
1014 let mut current_block_disabled = false;
1016
1017 for (i, line) in lines.iter().enumerate() {
1018 let line_num = i + 1;
1019 let trimmed = line.trim_start();
1020
1021 if !in_fenced_block
1024 && Self::has_valid_fence_indent(line)
1025 && (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
1026 {
1027 current_block_disabled = ctx.inline_config().is_rule_disabled(self.name(), line_num);
1029 in_fenced_block = true;
1030 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
1031 let opener_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1032 fenced_fence_opener = Some((fence_char, opener_len));
1033
1034 if current_block_disabled {
1035 result.push_str(line);
1037 result.push('\n');
1038 } else if target_style == CodeBlockStyle::Indented {
1039 in_indented_block = true;
1041 } else {
1042 result.push_str(line);
1044 result.push('\n');
1045 }
1046 } else if in_fenced_block && fenced_fence_opener.is_some() {
1047 let (fence_char, opener_len) = fenced_fence_opener.unwrap();
1048 let closer_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1051 let after_closer = &trimmed[closer_len..];
1052 let is_closer = closer_len >= opener_len && after_closer.trim().is_empty() && closer_len > 0;
1053 if is_closer {
1054 in_fenced_block = false;
1055 fenced_fence_opener = None;
1056 in_indented_block = false;
1057
1058 if current_block_disabled {
1059 result.push_str(line);
1060 result.push('\n');
1061 } else if target_style == CodeBlockStyle::Indented {
1062 } else {
1064 result.push_str(line);
1066 result.push('\n');
1067 }
1068 current_block_disabled = false;
1069 } else if current_block_disabled {
1070 result.push_str(line);
1072 result.push('\n');
1073 } else if target_style == CodeBlockStyle::Indented {
1074 result.push_str(" ");
1078 result.push_str(line);
1079 result.push('\n');
1080 } else {
1081 result.push_str(line);
1083 result.push('\n');
1084 }
1085 } else if self.is_indented_code_block_with_context(lines, i, is_mkdocs, &ictx) {
1086 if ctx.inline_config().is_rule_disabled(self.name(), line_num) {
1090 result.push_str(line);
1091 result.push('\n');
1092 continue;
1093 }
1094
1095 let prev_line_is_indented =
1097 i > 0 && self.is_indented_code_block_with_context(lines, i - 1, is_mkdocs, &ictx);
1098
1099 if target_style == CodeBlockStyle::Fenced {
1100 let baseline = list_item_baseline.get(i).copied().flatten().unwrap_or(0);
1106 let body = line.strip_prefix(" ").unwrap_or(line);
1112
1113 if misplaced_fence_lines[i] {
1116 result.push_str(line.trim_start());
1118 result.push('\n');
1119 } else if unsafe_fence_lines[i] {
1120 result.push_str(line);
1123 result.push('\n');
1124 } else if !prev_line_is_indented && !in_indented_block {
1125 current_block_fence_indent = " ".repeat(baseline);
1127 result.push_str(¤t_block_fence_indent);
1128 result.push_str("```\n");
1129 result.push_str(body);
1130 result.push('\n');
1131 in_indented_block = true;
1132 } else {
1133 result.push_str(body);
1135 result.push('\n');
1136 }
1137
1138 let next_line_is_indented =
1140 i < lines.len() - 1 && self.is_indented_code_block_with_context(lines, i + 1, is_mkdocs, &ictx);
1141 if !next_line_is_indented
1143 && in_indented_block
1144 && !misplaced_fence_lines[i]
1145 && !unsafe_fence_lines[i]
1146 {
1147 result.push_str(¤t_block_fence_indent);
1148 result.push_str("```\n");
1149 in_indented_block = false;
1150 current_block_fence_indent.clear();
1151 }
1152 } else {
1153 result.push_str(line);
1155 result.push('\n');
1156 }
1157 } else {
1158 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1160 result.push_str(¤t_block_fence_indent);
1161 result.push_str("```\n");
1162 in_indented_block = false;
1163 current_block_fence_indent.clear();
1164 }
1165
1166 result.push_str(line);
1167 result.push('\n');
1168 }
1169 }
1170
1171 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1173 result.push_str(¤t_block_fence_indent);
1174 result.push_str("```\n");
1175 }
1176
1177 if let Some((fence_char, opener_len)) = fenced_fence_opener
1183 && in_fenced_block
1184 {
1185 let has_unclosed_violation = !self.check_unclosed_code_blocks(ctx).is_empty();
1186 if has_unclosed_violation {
1187 let closer: String = std::iter::repeat_n(fence_char, opener_len).collect();
1188 result.push_str(&closer);
1189 result.push('\n');
1190 }
1191 }
1192
1193 if !content.ends_with('\n') && result.ends_with('\n') {
1195 result.pop();
1196 }
1197
1198 Ok(result)
1199 }
1200
1201 fn category(&self) -> RuleCategory {
1203 RuleCategory::CodeBlock
1204 }
1205
1206 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
1208 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
1211 }
1212
1213 fn as_any(&self) -> &dyn std::any::Any {
1214 self
1215 }
1216
1217 fn default_config_section(&self) -> Option<(String, toml::Value)> {
1218 let json_value = serde_json::to_value(&self.config).ok()?;
1219 Some((
1220 self.name().to_string(),
1221 crate::rule_config_serde::json_to_toml_value(&json_value)?,
1222 ))
1223 }
1224
1225 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1226 where
1227 Self: Sized,
1228 {
1229 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
1230 Box::new(Self::from_config_struct(rule_config))
1231 }
1232}
1233
1234#[cfg(test)]
1235mod tests {
1236 use super::*;
1237 use crate::lint_context::LintContext;
1238
1239 fn detect_style_from_content(rule: &MD046CodeBlockStyle, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
1251 let flavor = if is_mkdocs {
1252 crate::config::MarkdownFlavor::MkDocs
1253 } else {
1254 crate::config::MarkdownFlavor::Standard
1255 };
1256 let ctx = LintContext::new(content, flavor, None);
1257 let lines: Vec<&str> = content.lines().collect();
1258 let in_list_context = rule.precompute_block_continuation_context(&lines);
1259 let in_tab_context = if is_mkdocs {
1260 rule.precompute_mkdocs_tab_context(&lines)
1261 } else {
1262 vec![false; lines.len()]
1263 };
1264 let in_admonition_context = if is_mkdocs {
1265 rule.precompute_mkdocs_admonition_context(&lines)
1266 } else {
1267 vec![false; lines.len()]
1268 };
1269 let in_comment_or_html = vec![false; lines.len()];
1270 let list_item_baseline: Vec<Option<usize>> = vec![None; lines.len()];
1276 let ictx = IndentContext {
1277 in_list_context: &in_list_context,
1278 in_tab_context: &in_tab_context,
1279 in_admonition_context: &in_admonition_context,
1280 in_comment_or_html: &in_comment_or_html,
1281 list_item_baseline: &list_item_baseline,
1282 };
1283 rule.detect_style(&ctx, &lines, is_mkdocs, &ictx)
1284 }
1285
1286 #[test]
1287 fn test_fenced_code_block_detection() {
1288 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1289 assert!(rule.is_fenced_code_block_start("```"));
1290 assert!(rule.is_fenced_code_block_start("```rust"));
1291 assert!(rule.is_fenced_code_block_start("~~~"));
1292 assert!(rule.is_fenced_code_block_start("~~~python"));
1293 assert!(rule.is_fenced_code_block_start(" ```"));
1294 assert!(!rule.is_fenced_code_block_start("``"));
1295 assert!(!rule.is_fenced_code_block_start("~~"));
1296 assert!(!rule.is_fenced_code_block_start("Regular text"));
1297 }
1298
1299 #[test]
1300 fn test_consistent_style_with_fenced_blocks() {
1301 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1302 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
1303 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1304 let result = rule.check(&ctx).unwrap();
1305
1306 assert_eq!(result.len(), 0);
1308 }
1309
1310 #[test]
1311 fn test_consistent_style_with_indented_blocks() {
1312 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1313 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
1314 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1315 let result = rule.check(&ctx).unwrap();
1316
1317 assert_eq!(result.len(), 0);
1319 }
1320
1321 #[test]
1322 fn test_consistent_style_mixed() {
1323 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1324 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
1325 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1326 let result = rule.check(&ctx).unwrap();
1327
1328 assert!(!result.is_empty());
1330 }
1331
1332 #[test]
1333 fn test_fenced_style_with_indented_blocks() {
1334 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1335 let content = "Text\n\n indented code\n more code\n\nMore text";
1336 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1337 let result = rule.check(&ctx).unwrap();
1338
1339 assert!(!result.is_empty());
1341 assert!(result[0].message.contains("Use fenced code blocks"));
1342 }
1343
1344 #[test]
1345 fn test_fenced_style_with_tab_indented_blocks() {
1346 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1347 let content = "Text\n\n\ttab indented code\n\tmore code\n\nMore text";
1348 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1349 let result = rule.check(&ctx).unwrap();
1350
1351 assert!(!result.is_empty());
1353 assert!(result[0].message.contains("Use fenced code blocks"));
1354 }
1355
1356 #[test]
1357 fn test_fenced_style_with_mixed_whitespace_indented_blocks() {
1358 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1359 let content = "Text\n\n \tmixed indent code\n \tmore code\n\nMore text";
1361 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1362 let result = rule.check(&ctx).unwrap();
1363
1364 assert!(
1366 !result.is_empty(),
1367 "Mixed whitespace (2 spaces + tab) should be detected as indented code"
1368 );
1369 assert!(result[0].message.contains("Use fenced code blocks"));
1370 }
1371
1372 #[test]
1373 fn test_fenced_style_with_one_space_tab_indent() {
1374 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1375 let content = "Text\n\n \ttab after one space\n \tmore code\n\nMore text";
1377 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1378 let result = rule.check(&ctx).unwrap();
1379
1380 assert!(!result.is_empty(), "1 space + tab should be detected as indented code");
1381 assert!(result[0].message.contains("Use fenced code blocks"));
1382 }
1383
1384 #[test]
1385 fn test_indented_style_with_fenced_blocks() {
1386 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1387 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
1388 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1389 let result = rule.check(&ctx).unwrap();
1390
1391 assert!(!result.is_empty());
1393 assert!(result[0].message.contains("Use indented code blocks"));
1394 }
1395
1396 #[test]
1397 fn test_unclosed_code_block() {
1398 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1399 let content = "```\ncode without closing fence";
1400 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1401 let result = rule.check(&ctx).unwrap();
1402
1403 assert_eq!(result.len(), 1);
1404 assert!(result[0].message.contains("never closed"));
1405 }
1406
1407 #[test]
1408 fn test_nested_code_blocks() {
1409 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1410 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
1411 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1412 let result = rule.check(&ctx).unwrap();
1413
1414 assert_eq!(result.len(), 0);
1416 }
1417
1418 #[test]
1419 fn test_fix_indented_to_fenced() {
1420 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1421 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
1422 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1423 let fixed = rule.fix(&ctx).unwrap();
1424
1425 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
1426 }
1427
1428 #[test]
1429 fn test_fix_fenced_to_indented() {
1430 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1431 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
1432 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1433 let fixed = rule.fix(&ctx).unwrap();
1434
1435 assert!(fixed.contains(" code line 1\n code line 2"));
1436 assert!(!fixed.contains("```"));
1437 }
1438
1439 #[test]
1440 fn test_fix_fenced_to_indented_preserves_internal_indentation() {
1441 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1444 let content = r#"# Test
1445
1446```html
1447<!doctype html>
1448<html>
1449 <head>
1450 <title>Test</title>
1451 </head>
1452</html>
1453```
1454"#;
1455 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1456 let fixed = rule.fix(&ctx).unwrap();
1457
1458 assert!(
1461 fixed.contains(" <head>"),
1462 "Expected 6 spaces before <head> (4 for code block + 2 original), got:\n{fixed}"
1463 );
1464 assert!(
1465 fixed.contains(" <title>"),
1466 "Expected 8 spaces before <title> (4 for code block + 4 original), got:\n{fixed}"
1467 );
1468 assert!(!fixed.contains("```"), "Fenced markers should be removed");
1469 }
1470
1471 #[test]
1472 fn test_fix_fenced_to_indented_preserves_python_indentation() {
1473 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1475 let content = r#"# Python Example
1476
1477```python
1478def greet(name):
1479 if name:
1480 print(f"Hello, {name}!")
1481 else:
1482 print("Hello, World!")
1483```
1484"#;
1485 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1486 let fixed = rule.fix(&ctx).unwrap();
1487
1488 assert!(
1490 fixed.contains(" def greet(name):"),
1491 "Function def should have 4 spaces (code block indent)"
1492 );
1493 assert!(
1494 fixed.contains(" if name:"),
1495 "if statement should have 8 spaces (4 code + 4 Python)"
1496 );
1497 assert!(
1498 fixed.contains(" print"),
1499 "print should have 12 spaces (4 code + 8 Python)"
1500 );
1501 }
1502
1503 #[test]
1504 fn test_fix_fenced_to_indented_preserves_yaml_indentation() {
1505 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1507 let content = r#"# Config
1508
1509```yaml
1510server:
1511 host: localhost
1512 port: 8080
1513 ssl:
1514 enabled: true
1515 cert: /path/to/cert
1516```
1517"#;
1518 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1519 let fixed = rule.fix(&ctx).unwrap();
1520
1521 assert!(fixed.contains(" server:"), "Root key should have 4 spaces");
1522 assert!(fixed.contains(" host:"), "First level should have 6 spaces");
1523 assert!(fixed.contains(" ssl:"), "ssl key should have 6 spaces");
1524 assert!(fixed.contains(" enabled:"), "Nested ssl should have 8 spaces");
1525 }
1526
1527 #[test]
1528 fn test_fix_fenced_to_indented_preserves_empty_lines() {
1529 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1531 let content = "```\nline1\n\nline2\n```\n";
1532 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1533 let fixed = rule.fix(&ctx).unwrap();
1534
1535 assert!(fixed.contains(" line1"), "line1 should be indented");
1537 assert!(fixed.contains(" line2"), "line2 should be indented");
1538 }
1540
1541 #[test]
1542 fn test_fix_fenced_to_indented_multiple_blocks() {
1543 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1545 let content = r#"# Doc
1546
1547```python
1548def foo():
1549 pass
1550```
1551
1552Text between.
1553
1554```yaml
1555key:
1556 value: 1
1557```
1558"#;
1559 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1560 let fixed = rule.fix(&ctx).unwrap();
1561
1562 assert!(fixed.contains(" def foo():"), "Python def should be indented");
1563 assert!(fixed.contains(" pass"), "Python body should have 8 spaces");
1564 assert!(fixed.contains(" key:"), "YAML root should have 4 spaces");
1565 assert!(fixed.contains(" value:"), "YAML nested should have 6 spaces");
1566 assert!(!fixed.contains("```"), "No fence markers should remain");
1567 }
1568
1569 #[test]
1570 fn test_fix_unclosed_block() {
1571 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1572 let content = "```\ncode without closing";
1573 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1574 let fixed = rule.fix(&ctx).unwrap();
1575
1576 assert!(fixed.ends_with("```"));
1578 }
1579
1580 #[test]
1581 fn test_code_block_in_list() {
1582 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1583 let content = "- List item\n code in list\n more code\n- Next item";
1584 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1585 let result = rule.check(&ctx).unwrap();
1586
1587 assert_eq!(result.len(), 0);
1589 }
1590
1591 #[test]
1592 fn test_detect_style_fenced() {
1593 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1594 let content = "```\ncode\n```";
1595 let style = detect_style_from_content(&rule, content, false);
1596
1597 assert_eq!(style, Some(CodeBlockStyle::Fenced));
1598 }
1599
1600 #[test]
1601 fn test_detect_style_indented() {
1602 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1603 let content = "Text\n\n code\n\nMore";
1604 let style = detect_style_from_content(&rule, content, false);
1605
1606 assert_eq!(style, Some(CodeBlockStyle::Indented));
1607 }
1608
1609 #[test]
1610 fn test_detect_style_none() {
1611 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1612 let content = "No code blocks here";
1613 let style = detect_style_from_content(&rule, content, false);
1614
1615 assert_eq!(style, None);
1616 }
1617
1618 #[test]
1619 fn test_tilde_fence() {
1620 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1621 let content = "~~~\ncode\n~~~";
1622 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1623 let result = rule.check(&ctx).unwrap();
1624
1625 assert_eq!(result.len(), 0);
1627 }
1628
1629 #[test]
1630 fn test_language_specification() {
1631 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1632 let content = "```rust\nfn main() {}\n```";
1633 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1634 let result = rule.check(&ctx).unwrap();
1635
1636 assert_eq!(result.len(), 0);
1637 }
1638
1639 #[test]
1640 fn test_empty_content() {
1641 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1642 let content = "";
1643 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1644 let result = rule.check(&ctx).unwrap();
1645
1646 assert_eq!(result.len(), 0);
1647 }
1648
1649 #[test]
1650 fn test_default_config() {
1651 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1652 let (name, _config) = rule.default_config_section().unwrap();
1653 assert_eq!(name, "MD046");
1654 }
1655
1656 #[test]
1657 fn test_markdown_documentation_block() {
1658 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1659 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
1660 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1661 let result = rule.check(&ctx).unwrap();
1662
1663 assert_eq!(result.len(), 0);
1665 }
1666
1667 #[test]
1668 fn test_preserve_trailing_newline() {
1669 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1670 let content = "```\ncode\n```\n";
1671 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1672 let fixed = rule.fix(&ctx).unwrap();
1673
1674 assert_eq!(fixed, content);
1675 }
1676
1677 #[test]
1678 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
1679 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1680 let content = r#"# Document
1681
1682=== "Python"
1683
1684 This is tab content
1685 Not an indented code block
1686
1687 ```python
1688 def hello():
1689 print("Hello")
1690 ```
1691
1692=== "JavaScript"
1693
1694 More tab content here
1695 Also not an indented code block"#;
1696
1697 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1698 let result = rule.check(&ctx).unwrap();
1699
1700 assert_eq!(result.len(), 0);
1702 }
1703
1704 #[test]
1705 fn test_mkdocs_tabs_with_actual_indented_code() {
1706 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1707 let content = r#"# Document
1708
1709=== "Tab 1"
1710
1711 This is tab content
1712
1713Regular text
1714
1715 This is an actual indented code block
1716 Should be flagged"#;
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(), 1);
1723 assert!(result[0].message.contains("Use fenced code blocks"));
1724 }
1725
1726 #[test]
1727 fn test_mkdocs_tabs_detect_style() {
1728 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1729 let content = r#"=== "Tab 1"
1730
1731 Content in tab
1732 More content
1733
1734=== "Tab 2"
1735
1736 Content in second tab"#;
1737
1738 let style = detect_style_from_content(&rule, content, true);
1740 assert_eq!(style, None); let style = detect_style_from_content(&rule, content, false);
1744 assert_eq!(style, Some(CodeBlockStyle::Indented));
1745 }
1746
1747 #[test]
1748 fn test_mkdocs_nested_tabs() {
1749 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1750 let content = r#"# Document
1751
1752=== "Outer Tab"
1753
1754 Some content
1755
1756 === "Nested Tab"
1757
1758 Nested tab content
1759 Should not be flagged"#;
1760
1761 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1762 let result = rule.check(&ctx).unwrap();
1763
1764 assert_eq!(result.len(), 0);
1766 }
1767
1768 #[test]
1769 fn test_mkdocs_admonitions_not_flagged_as_indented_code() {
1770 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1773 let content = r#"# Document
1774
1775!!! note
1776 This is normal admonition content, not a code block.
1777 It spans multiple lines.
1778
1779??? warning "Collapsible Warning"
1780 This is also admonition content.
1781
1782???+ tip "Expanded Tip"
1783 And this one too.
1784
1785Regular text outside admonitions."#;
1786
1787 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1788 let result = rule.check(&ctx).unwrap();
1789
1790 assert_eq!(
1792 result.len(),
1793 0,
1794 "Admonition content in MkDocs mode should not trigger MD046"
1795 );
1796 }
1797
1798 #[test]
1799 fn test_mkdocs_admonition_with_actual_indented_code() {
1800 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1802 let content = r#"# Document
1803
1804!!! note
1805 This is admonition content.
1806
1807Regular text ends the admonition.
1808
1809 This is actual indented code (should be flagged)"#;
1810
1811 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1812 let result = rule.check(&ctx).unwrap();
1813
1814 assert_eq!(result.len(), 1);
1816 assert!(result[0].message.contains("Use fenced code blocks"));
1817 }
1818
1819 #[test]
1820 fn test_admonition_in_standard_mode_flagged() {
1821 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1825 let content = r#"# Document
1826
1827!!! note
1828
1829 This looks like code in standard mode.
1830
1831Regular text."#;
1832
1833 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1835 let result = rule.check(&ctx).unwrap();
1836
1837 assert_eq!(
1839 result.len(),
1840 1,
1841 "Admonition content in Standard mode should be flagged as indented code"
1842 );
1843 }
1844
1845 #[test]
1846 fn test_mkdocs_admonition_with_fenced_code_inside() {
1847 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1849 let content = r#"# Document
1850
1851!!! note "Code Example"
1852 Here's some code:
1853
1854 ```python
1855 def hello():
1856 print("world")
1857 ```
1858
1859 More text after code.
1860
1861Regular text."#;
1862
1863 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1864 let result = rule.check(&ctx).unwrap();
1865
1866 assert_eq!(result.len(), 0, "Fenced code blocks inside admonitions should be valid");
1868 }
1869
1870 #[test]
1871 fn test_mkdocs_nested_admonitions() {
1872 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1874 let content = r#"# Document
1875
1876!!! note "Outer"
1877 Outer content.
1878
1879 !!! warning "Inner"
1880 Inner content.
1881 More inner content.
1882
1883 Back to outer.
1884
1885Regular text."#;
1886
1887 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1888 let result = rule.check(&ctx).unwrap();
1889
1890 assert_eq!(result.len(), 0, "Nested admonitions should not be flagged");
1892 }
1893
1894 #[test]
1895 fn test_mkdocs_admonition_fix_does_not_wrap() {
1896 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1898 let content = r#"!!! note
1899 Content that should stay as admonition content.
1900 Not be wrapped in code fences.
1901"#;
1902
1903 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1904 let fixed = rule.fix(&ctx).unwrap();
1905
1906 assert!(
1908 !fixed.contains("```\n Content"),
1909 "Admonition content should not be wrapped in fences"
1910 );
1911 assert_eq!(fixed, content, "Content should remain unchanged");
1912 }
1913
1914 #[test]
1915 fn test_mkdocs_empty_admonition() {
1916 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1918 let content = r#"!!! note
1919
1920Regular paragraph after empty admonition.
1921
1922 This IS an indented code block (after blank + non-indented line)."#;
1923
1924 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1925 let result = rule.check(&ctx).unwrap();
1926
1927 assert_eq!(result.len(), 1, "Indented code after admonition ends should be flagged");
1929 }
1930
1931 #[test]
1932 fn test_mkdocs_indented_admonition() {
1933 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1935 let content = r#"- List item
1936
1937 !!! note
1938 Indented admonition content.
1939 More content.
1940
1941- Next item"#;
1942
1943 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1944 let result = rule.check(&ctx).unwrap();
1945
1946 assert_eq!(
1948 result.len(),
1949 0,
1950 "Indented admonitions (e.g., in lists) should not be flagged"
1951 );
1952 }
1953
1954 #[test]
1955 fn test_footnote_indented_paragraphs_not_flagged() {
1956 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1957 let content = r#"# Test Document with Footnotes
1958
1959This is some text with a footnote[^1].
1960
1961Here's some code:
1962
1963```bash
1964echo "fenced code block"
1965```
1966
1967More text with another footnote[^2].
1968
1969[^1]: Really interesting footnote text.
1970
1971 Even more interesting second paragraph.
1972
1973[^2]: Another footnote.
1974
1975 With a second paragraph too.
1976
1977 And even a third paragraph!"#;
1978
1979 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1980 let result = rule.check(&ctx).unwrap();
1981
1982 assert_eq!(result.len(), 0);
1984 }
1985
1986 #[test]
1987 fn test_footnote_definition_detection() {
1988 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1989
1990 assert!(rule.is_footnote_definition("[^1]: Footnote text"));
1993 assert!(rule.is_footnote_definition("[^foo]: Footnote text"));
1994 assert!(rule.is_footnote_definition("[^long-name]: Footnote text"));
1995 assert!(rule.is_footnote_definition("[^test_123]: Mixed chars"));
1996 assert!(rule.is_footnote_definition(" [^1]: Indented footnote"));
1997 assert!(rule.is_footnote_definition("[^a]: Minimal valid footnote"));
1998 assert!(rule.is_footnote_definition("[^123]: Numeric label"));
1999 assert!(rule.is_footnote_definition("[^_]: Single underscore"));
2000 assert!(rule.is_footnote_definition("[^-]: Single hyphen"));
2001
2002 assert!(!rule.is_footnote_definition("[^]: No label"));
2004 assert!(!rule.is_footnote_definition("[^ ]: Whitespace only"));
2005 assert!(!rule.is_footnote_definition("[^ ]: Multiple spaces"));
2006 assert!(!rule.is_footnote_definition("[^\t]: Tab only"));
2007
2008 assert!(!rule.is_footnote_definition("[^]]: Extra bracket"));
2010 assert!(!rule.is_footnote_definition("Regular text [^1]:"));
2011 assert!(!rule.is_footnote_definition("[1]: Not a footnote"));
2012 assert!(!rule.is_footnote_definition("[^")); assert!(!rule.is_footnote_definition("[^1:")); assert!(!rule.is_footnote_definition("^1]: Missing opening bracket"));
2015
2016 assert!(!rule.is_footnote_definition("[^test.name]: Period"));
2018 assert!(!rule.is_footnote_definition("[^test name]: Space in label"));
2019 assert!(!rule.is_footnote_definition("[^test@name]: Special char"));
2020 assert!(!rule.is_footnote_definition("[^test/name]: Slash"));
2021 assert!(!rule.is_footnote_definition("[^test\\name]: Backslash"));
2022
2023 assert!(!rule.is_footnote_definition("[^test\r]: Carriage return"));
2026 }
2027
2028 #[test]
2029 fn test_footnote_with_blank_lines() {
2030 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2034 let content = r#"# Document
2035
2036Text with footnote[^1].
2037
2038[^1]: First paragraph.
2039
2040 Second paragraph after blank line.
2041
2042 Third paragraph after another blank line.
2043
2044Regular text at column 0 ends the footnote."#;
2045
2046 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2047 let result = rule.check(&ctx).unwrap();
2048
2049 assert_eq!(
2051 result.len(),
2052 0,
2053 "Indented content within footnotes should not trigger MD046"
2054 );
2055 }
2056
2057 #[test]
2058 fn test_footnote_multiple_consecutive_blank_lines() {
2059 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2062 let content = r#"Text[^1].
2063
2064[^1]: First paragraph.
2065
2066
2067
2068 Content after three blank lines (still part of footnote).
2069
2070Not indented, so footnote ends here."#;
2071
2072 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2073 let result = rule.check(&ctx).unwrap();
2074
2075 assert_eq!(
2077 result.len(),
2078 0,
2079 "Multiple blank lines shouldn't break footnote continuation"
2080 );
2081 }
2082
2083 #[test]
2084 fn test_footnote_terminated_by_non_indented_content() {
2085 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2088 let content = r#"[^1]: Footnote content.
2089
2090 More indented content in footnote.
2091
2092This paragraph is not indented, so footnote ends.
2093
2094 This should be flagged as indented code block."#;
2095
2096 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2097 let result = rule.check(&ctx).unwrap();
2098
2099 assert_eq!(
2101 result.len(),
2102 1,
2103 "Indented code after footnote termination should be flagged"
2104 );
2105 assert!(
2106 result[0].message.contains("Use fenced code blocks"),
2107 "Expected MD046 warning for indented code block"
2108 );
2109 assert!(result[0].line >= 7, "Warning should be on the indented code block line");
2110 }
2111
2112 #[test]
2113 fn test_footnote_terminated_by_structural_elements() {
2114 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2116 let content = r#"[^1]: Footnote content.
2117
2118 More content.
2119
2120## Heading terminates footnote
2121
2122 This indented content should be flagged.
2123
2124---
2125
2126 This should also be flagged (after horizontal rule)."#;
2127
2128 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2129 let result = rule.check(&ctx).unwrap();
2130
2131 assert_eq!(
2133 result.len(),
2134 2,
2135 "Both indented blocks after termination should be flagged"
2136 );
2137 }
2138
2139 #[test]
2140 fn test_footnote_with_code_block_inside() {
2141 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2144 let content = r#"Text[^1].
2145
2146[^1]: Footnote with code:
2147
2148 ```python
2149 def hello():
2150 print("world")
2151 ```
2152
2153 More footnote text after code."#;
2154
2155 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2156 let result = rule.check(&ctx).unwrap();
2157
2158 assert_eq!(result.len(), 0, "Fenced code blocks within footnotes should be allowed");
2160 }
2161
2162 #[test]
2163 fn test_footnote_with_8_space_indented_code() {
2164 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2167 let content = r#"Text[^1].
2168
2169[^1]: Footnote with nested code.
2170
2171 code block
2172 more code"#;
2173
2174 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2175 let result = rule.check(&ctx).unwrap();
2176
2177 assert_eq!(
2179 result.len(),
2180 0,
2181 "8-space indented code within footnotes represents nested code blocks"
2182 );
2183 }
2184
2185 #[test]
2186 fn test_multiple_footnotes() {
2187 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2190 let content = r#"Text[^1] and more[^2].
2191
2192[^1]: First footnote.
2193
2194 Continuation of first.
2195
2196[^2]: Second footnote starts here, ending the first.
2197
2198 Continuation of second."#;
2199
2200 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2201 let result = rule.check(&ctx).unwrap();
2202
2203 assert_eq!(
2205 result.len(),
2206 0,
2207 "Multiple footnotes should each maintain their continuation context"
2208 );
2209 }
2210
2211 #[test]
2212 fn test_list_item_ends_footnote_context() {
2213 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2215 let content = r#"[^1]: Footnote.
2216
2217 Content in footnote.
2218
2219- List item starts here (ends footnote context).
2220
2221 This indented content is part of the list, not the footnote."#;
2222
2223 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2224 let result = rule.check(&ctx).unwrap();
2225
2226 assert_eq!(
2228 result.len(),
2229 0,
2230 "List items should end footnote context and start their own"
2231 );
2232 }
2233
2234 #[test]
2235 fn test_footnote_vs_actual_indented_code() {
2236 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2239 let content = r#"# Heading
2240
2241Text with footnote[^1].
2242
2243[^1]: Footnote content.
2244
2245 Part of footnote (should not be flagged).
2246
2247Regular paragraph ends footnote context.
2248
2249 This is actual indented code (MUST be flagged)
2250 Should be detected as code block"#;
2251
2252 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2253 let result = rule.check(&ctx).unwrap();
2254
2255 assert_eq!(
2257 result.len(),
2258 1,
2259 "Must still detect indented code blocks outside footnotes"
2260 );
2261 assert!(
2262 result[0].message.contains("Use fenced code blocks"),
2263 "Expected MD046 warning for indented code"
2264 );
2265 assert!(
2266 result[0].line >= 11,
2267 "Warning should be on the actual indented code line"
2268 );
2269 }
2270
2271 #[test]
2272 fn test_spec_compliant_label_characters() {
2273 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2276
2277 assert!(rule.is_footnote_definition("[^test]: text"));
2279 assert!(rule.is_footnote_definition("[^TEST]: text"));
2280 assert!(rule.is_footnote_definition("[^test-name]: text"));
2281 assert!(rule.is_footnote_definition("[^test_name]: text"));
2282 assert!(rule.is_footnote_definition("[^test123]: text"));
2283 assert!(rule.is_footnote_definition("[^123]: text"));
2284 assert!(rule.is_footnote_definition("[^a1b2c3]: text"));
2285
2286 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")); }
2294
2295 #[test]
2296 fn test_code_block_inside_html_comment() {
2297 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2300 let content = r#"# Document
2301
2302Some text.
2303
2304<!--
2305Example code block in comment:
2306
2307```typescript
2308console.log("Hello");
2309```
2310
2311More comment text.
2312-->
2313
2314More content."#;
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.len(),
2321 0,
2322 "Code blocks inside HTML comments should not be flagged as unclosed"
2323 );
2324 }
2325
2326 #[test]
2327 fn test_unclosed_fence_inside_html_comment() {
2328 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2330 let content = r#"# Document
2331
2332<!--
2333Example with intentionally unclosed fence:
2334
2335```
2336code without closing
2337-->
2338
2339More content."#;
2340
2341 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2342 let result = rule.check(&ctx).unwrap();
2343
2344 assert_eq!(
2345 result.len(),
2346 0,
2347 "Unclosed fences inside HTML comments should be ignored"
2348 );
2349 }
2350
2351 #[test]
2352 fn test_multiline_html_comment_with_indented_code() {
2353 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2355 let content = r#"# Document
2356
2357<!--
2358Example:
2359
2360 indented code
2361 more code
2362
2363End of comment.
2364-->
2365
2366Regular text."#;
2367
2368 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2369 let result = rule.check(&ctx).unwrap();
2370
2371 assert_eq!(
2372 result.len(),
2373 0,
2374 "Indented code inside HTML comments should not be flagged"
2375 );
2376 }
2377
2378 #[test]
2379 fn test_code_block_after_html_comment() {
2380 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2382 let content = r#"# Document
2383
2384<!-- comment -->
2385
2386Text before.
2387
2388 indented code should be flagged
2389
2390More text."#;
2391
2392 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2393 let result = rule.check(&ctx).unwrap();
2394
2395 assert_eq!(
2396 result.len(),
2397 1,
2398 "Code blocks after HTML comments should still be detected"
2399 );
2400 assert!(result[0].message.contains("Use fenced code blocks"));
2401 }
2402
2403 #[test]
2404 fn test_consistent_style_indented_html_comment() {
2405 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2411 let content = "# MD046 false-positive reproduction\n\
2412 \n\
2413 <!--\n \
2414 This is just an indented comment, not a code block.\n\
2415 \n \
2416 A second line is required to trigger the false-positive.\n\
2417 \n \
2418 Actually, three lines are required.\n\
2419 -->\n\
2420 \n\
2421 ```md\n\
2422 This should be fine, since it's the only code block and therefore consistent.\n\
2423 ```\n";
2424
2425 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2426 let result = rule.check(&ctx).unwrap();
2427
2428 assert_eq!(
2429 result,
2430 vec![],
2431 "A single fenced block and an indented HTML comment must produce no MD046 warnings",
2432 );
2433 }
2434
2435 #[test]
2436 fn test_consistent_style_indented_html_block() {
2437 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2444 let content = "# Heading\n\
2445 \n\
2446 <div class=\"note\">\n \
2447 line one of indented html content\n \
2448 line two of indented html content\n \
2449 line three of indented html content\n\
2450 </div>\n\
2451 \n\
2452 ```md\n\
2453 real fenced block\n\
2454 ```\n";
2455
2456 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2457 let result = rule.check(&ctx).unwrap();
2458
2459 assert_eq!(
2460 result,
2461 vec![],
2462 "Indented content inside a raw HTML block must not influence MD046 style detection",
2463 );
2464 }
2465
2466 #[test]
2467 fn test_consistent_style_fake_fence_inside_html_comment() {
2468 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2474 let content = "# Title\n\
2475 \n\
2476 <!--\n\
2477 ```\n\
2478 fake fence inside comment\n\
2479 ```\n\
2480 -->\n\
2481 \n \
2482 real indented code block line 1\n \
2483 real indented code block line 2\n";
2484
2485 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2486 let result = rule.check(&ctx).unwrap();
2487
2488 assert_eq!(
2489 result,
2490 vec![],
2491 "Fence markers inside an HTML comment must not influence MD046 style detection",
2492 );
2493 }
2494
2495 #[test]
2496 fn test_consistent_style_indented_footnote_definition() {
2497 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2501 let content = "# Heading\n\
2502 \n\
2503 Reference to a footnote[^note].\n\
2504 \n\
2505 [^note]: First line of the footnote.\n \
2506 Second indented continuation line.\n \
2507 Third indented continuation line.\n \
2508 Fourth indented continuation line.\n\
2509 \n\
2510 ```md\n\
2511 real fenced block\n\
2512 ```\n";
2513
2514 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2515 let result = rule.check(&ctx).unwrap();
2516
2517 assert_eq!(
2518 result,
2519 vec![],
2520 "Footnote-definition continuation content must not influence MD046 style detection",
2521 );
2522 }
2523
2524 #[test]
2525 fn test_consistent_style_indented_blockquote() {
2526 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2531 let content = "# Heading\n\
2532 \n\
2533 > line one of quoted indented content\n\
2534 >\n\
2535 > line two of quoted indented content\n\
2536 >\n\
2537 > line three of quoted indented content\n\
2538 \n\
2539 ```md\n\
2540 real fenced block\n\
2541 ```\n";
2542
2543 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2544 let result = rule.check(&ctx).unwrap();
2545
2546 assert_eq!(
2547 result,
2548 vec![],
2549 "Indented content inside a blockquote must not influence MD046 style detection",
2550 );
2551 }
2552
2553 #[test]
2554 fn test_consistent_style_genuine_indented_block_detected_as_indented() {
2555 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2560 let content = "# Heading\n\
2561 \n\
2562 Some prose.\n\
2563 \n \
2564 real indented code line 1\n \
2565 real indented code line 2\n";
2566
2567 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2568 let result = rule.check(&ctx).unwrap();
2569
2570 assert_eq!(
2573 result,
2574 vec![],
2575 "A genuine top-level indented block must be detected as Indented style under Consistent",
2576 );
2577 }
2578
2579 #[test]
2580 fn test_consistent_style_skipped_lines_dont_override_real_block() {
2581 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2586 let content = "# Heading\n\
2587 \n\
2588 <!--\n \
2589 skipped indented comment line 1\n \
2590 skipped indented comment line 2\n\
2591 -->\n\
2592 \n\
2593 <!--\n \
2594 second skipped region\n \
2595 also skipped\n\
2596 -->\n\
2597 \n \
2598 real indented code line\n";
2599
2600 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2601 let result = rule.check(&ctx).unwrap();
2602
2603 assert_eq!(
2604 result,
2605 vec![],
2606 "Skipped container lines must not outweigh the single real indented block",
2607 );
2608 }
2609
2610 #[test]
2611 fn test_consistent_style_fenced_wins_over_skipped_indented() {
2612 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2616 let content = "# Heading\n\
2617 \n\
2618 <!--\n \
2619 skipped indented region one\n \
2620 more of region one\n\
2621 -->\n\
2622 \n\
2623 <!--\n \
2624 skipped indented region two\n \
2625 more of region two\n\
2626 -->\n\
2627 \n\
2628 ```md\n\
2629 real fenced block\n\
2630 ```\n";
2631
2632 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2633 let result = rule.check(&ctx).unwrap();
2634
2635 assert_eq!(
2636 result,
2637 vec![],
2638 "Fenced block must win when all indented lines are inside skipped containers",
2639 );
2640 }
2641
2642 #[test]
2643 fn test_four_space_indented_fence_is_not_valid_fence() {
2644 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2647
2648 assert!(rule.is_fenced_code_block_start("```"));
2650 assert!(rule.is_fenced_code_block_start(" ```"));
2651 assert!(rule.is_fenced_code_block_start(" ```"));
2652 assert!(rule.is_fenced_code_block_start(" ```"));
2653
2654 assert!(!rule.is_fenced_code_block_start(" ```"));
2656 assert!(!rule.is_fenced_code_block_start(" ```"));
2657 assert!(!rule.is_fenced_code_block_start(" ```"));
2658
2659 assert!(!rule.is_fenced_code_block_start("\t```"));
2661 }
2662
2663 #[test]
2664 fn test_issue_237_indented_fenced_block_detected_as_indented() {
2665 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2671
2672 let content = r#"## Test
2674
2675 ```js
2676 var foo = "hello";
2677 ```
2678"#;
2679
2680 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2681 let result = rule.check(&ctx).unwrap();
2682
2683 assert_eq!(
2685 result.len(),
2686 1,
2687 "4-space indented fence should be detected as indented code block"
2688 );
2689 assert!(
2690 result[0].message.contains("Use fenced code blocks"),
2691 "Expected 'Use fenced code blocks' message"
2692 );
2693 }
2694
2695 #[test]
2696 fn test_issue_276_indented_code_in_list() {
2697 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2700
2701 let content = r#"1. First item
27022. Second item with code:
2703
2704 # This is a code block in a list
2705 print("Hello, world!")
2706
27074. Third item"#;
2708
2709 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2710 let result = rule.check(&ctx).unwrap();
2711
2712 assert!(
2714 !result.is_empty(),
2715 "Indented code block inside list should be flagged when style=fenced"
2716 );
2717 assert!(
2718 result[0].message.contains("Use fenced code blocks"),
2719 "Expected 'Use fenced code blocks' message"
2720 );
2721 }
2722
2723 #[test]
2724 fn test_three_space_indented_fence_is_valid() {
2725 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2727
2728 let content = r#"## Test
2729
2730 ```js
2731 var foo = "hello";
2732 ```
2733"#;
2734
2735 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2736 let result = rule.check(&ctx).unwrap();
2737
2738 assert_eq!(
2740 result.len(),
2741 0,
2742 "3-space indented fence should be recognized as valid fenced code block"
2743 );
2744 }
2745
2746 #[test]
2747 fn test_indented_style_with_deeply_indented_fenced() {
2748 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2751
2752 let content = r#"Text
2753
2754 ```js
2755 var foo = "hello";
2756 ```
2757
2758More text
2759"#;
2760
2761 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2762 let result = rule.check(&ctx).unwrap();
2763
2764 assert_eq!(
2767 result.len(),
2768 0,
2769 "4-space indented content should be valid when style=indented"
2770 );
2771 }
2772
2773 #[test]
2774 fn test_fix_misplaced_fenced_block() {
2775 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2778
2779 let content = r#"## Test
2780
2781 ```js
2782 var foo = "hello";
2783 ```
2784"#;
2785
2786 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2787 let fixed = rule.fix(&ctx).unwrap();
2788
2789 let expected = r#"## Test
2791
2792```js
2793var foo = "hello";
2794```
2795"#;
2796
2797 assert_eq!(fixed, expected, "Fix should remove indentation, not add more fences");
2798 }
2799
2800 #[test]
2801 fn test_fix_regular_indented_block() {
2802 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2805
2806 let content = r#"Text
2807
2808 var foo = "hello";
2809 console.log(foo);
2810
2811More text
2812"#;
2813
2814 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2815 let fixed = rule.fix(&ctx).unwrap();
2816
2817 assert!(fixed.contains("```\nvar foo"), "Should add opening fence");
2819 assert!(fixed.contains("console.log(foo);\n```"), "Should add closing fence");
2820 }
2821
2822 #[test]
2823 fn test_fix_indented_block_with_fence_like_content() {
2824 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2828
2829 let content = r#"Text
2830
2831 some code
2832 ```not a fence opener
2833 more code
2834"#;
2835
2836 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2837 let fixed = rule.fix(&ctx).unwrap();
2838
2839 assert!(fixed.contains(" some code"), "Unsafe block should be left unchanged");
2841 assert!(!fixed.contains("```\nsome code"), "Should NOT wrap unsafe block");
2842 }
2843
2844 #[test]
2845 fn test_fix_mixed_indented_and_misplaced_blocks() {
2846 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2848
2849 let content = r#"Text
2850
2851 regular indented code
2852
2853More text
2854
2855 ```python
2856 print("hello")
2857 ```
2858"#;
2859
2860 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2861 let fixed = rule.fix(&ctx).unwrap();
2862
2863 assert!(
2865 fixed.contains("```\nregular indented code\n```"),
2866 "First block should be wrapped in fences"
2867 );
2868
2869 assert!(
2871 fixed.contains("\n```python\nprint(\"hello\")\n```"),
2872 "Second block should be dedented, not double-wrapped"
2873 );
2874 assert!(
2876 !fixed.contains("```\n```python"),
2877 "Should not have nested fence openers"
2878 );
2879 }
2880}