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