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
39struct OwnedIndentContext {
42 in_list_context: Vec<bool>,
43 in_tab_context: Vec<bool>,
44 in_admonition_context: Vec<bool>,
45 in_comment_or_html: Vec<bool>,
46 list_item_baseline: Vec<Option<usize>>,
47}
48
49impl OwnedIndentContext {
50 fn borrow(&self) -> IndentContext<'_> {
51 IndentContext {
52 in_list_context: &self.in_list_context,
53 in_tab_context: &self.in_tab_context,
54 in_admonition_context: &self.in_admonition_context,
55 in_comment_or_html: &self.in_comment_or_html,
56 list_item_baseline: &self.list_item_baseline,
57 }
58 }
59}
60
61#[derive(Clone)]
67pub struct MD046CodeBlockStyle {
68 config: MD046Config,
69}
70
71impl MD046CodeBlockStyle {
72 pub fn new(style: CodeBlockStyle) -> Self {
73 Self {
74 config: MD046Config { style },
75 }
76 }
77
78 pub fn from_config_struct(config: MD046Config) -> Self {
79 Self { config }
80 }
81
82 fn has_valid_fence_indent(line: &str) -> bool {
87 calculate_indentation_width_default(line) < 4
88 }
89
90 fn is_fenced_code_block_start(&self, line: &str) -> bool {
99 if !Self::has_valid_fence_indent(line) {
100 return false;
101 }
102
103 let trimmed = line.trim_start();
104 trimmed.starts_with("```") || trimmed.starts_with("~~~")
105 }
106
107 fn is_list_item(&self, line: &str) -> bool {
108 let trimmed = line.trim_start();
109 if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ") {
110 return true;
111 }
112 let after_digits = trimmed.trim_start_matches(|c: char| c.is_ascii_digit());
117 after_digits.len() < trimmed.len() && (after_digits.starts_with(". ") || after_digits.starts_with(") "))
118 }
119
120 fn is_footnote_definition(&self, line: &str) -> bool {
140 let trimmed = line.trim_start();
141 if !trimmed.starts_with("[^") || trimmed.len() < 5 {
142 return false;
143 }
144
145 if let Some(close_bracket_pos) = trimmed.find("]:")
146 && close_bracket_pos > 2
147 {
148 let label = &trimmed[2..close_bracket_pos];
149
150 if label.trim().is_empty() {
151 return false;
152 }
153
154 if label.contains('\r') {
156 return false;
157 }
158
159 if label.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
161 return true;
162 }
163 }
164
165 false
166 }
167
168 fn precompute_block_continuation_context(&self, lines: &[&str]) -> Vec<bool> {
191 let mut in_continuation_context = vec![false; lines.len()];
192 let mut last_list_item_line: Option<usize> = None;
193 let mut last_footnote_line: Option<usize> = None;
194 let mut blank_line_count = 0;
195
196 for (i, line) in lines.iter().enumerate() {
197 let trimmed = line.trim_start();
198 let indent_len = line.len() - trimmed.len();
199
200 if self.is_list_item(line) {
202 last_list_item_line = Some(i);
203 last_footnote_line = None; blank_line_count = 0;
205 in_continuation_context[i] = true;
206 continue;
207 }
208
209 if self.is_footnote_definition(line) {
211 last_footnote_line = Some(i);
212 last_list_item_line = None; blank_line_count = 0;
214 in_continuation_context[i] = true;
215 continue;
216 }
217
218 if line.trim().is_empty() {
220 if last_list_item_line.is_some() || last_footnote_line.is_some() {
222 blank_line_count += 1;
223 in_continuation_context[i] = true;
224
225 }
229 continue;
230 }
231
232 if indent_len == 0 && !trimmed.is_empty() {
234 if trimmed.starts_with('#') {
238 last_list_item_line = None;
239 last_footnote_line = None;
240 blank_line_count = 0;
241 continue;
242 }
243
244 if trimmed.starts_with("---") || trimmed.starts_with("***") {
246 last_list_item_line = None;
247 last_footnote_line = None;
248 blank_line_count = 0;
249 continue;
250 }
251
252 if let Some(list_line) = last_list_item_line
255 && (i - list_line > 5 || blank_line_count > 1)
256 {
257 last_list_item_line = None;
258 }
259
260 if last_footnote_line.is_some() {
262 last_footnote_line = None;
263 }
264
265 blank_line_count = 0;
266
267 if last_list_item_line.is_none() && last_footnote_line.is_some() {
269 last_footnote_line = None;
270 }
271 continue;
272 }
273
274 if indent_len > 0 && (last_list_item_line.is_some() || last_footnote_line.is_some()) {
276 in_continuation_context[i] = true;
277 blank_line_count = 0;
278 }
279 }
280
281 in_continuation_context
282 }
283
284 fn precompute_list_item_baseline(
295 &self,
296 ctx: &crate::lint_context::LintContext,
297 lines: &[&str],
298 ) -> Vec<Option<usize>> {
299 let mut baselines = vec![None; lines.len()];
300 let mut last_baseline: Option<usize> = None;
301 let mut last_list_item_line: Option<usize> = None;
302 let mut blank_line_count = 0usize;
303
304 for (i, line) in lines.iter().enumerate() {
305 let trimmed = line.trim_start();
306 let indent_len = line.len() - trimmed.len();
307
308 if let Some(item) = ctx.line_info(i + 1).and_then(|li| li.list_item.as_ref()) {
310 last_baseline = Some(item.content_column);
311 last_list_item_line = Some(i);
312 blank_line_count = 0;
313 baselines[i] = last_baseline;
314 continue;
315 }
316
317 if line.trim().is_empty() {
319 if last_baseline.is_some() {
320 blank_line_count += 1;
321 baselines[i] = last_baseline;
322 }
323 continue;
324 }
325
326 if indent_len == 0 {
330 if trimmed.starts_with('#') || trimmed.starts_with("---") || trimmed.starts_with("***") {
331 last_baseline = None;
332 last_list_item_line = None;
333 } else if let Some(list_line) = last_list_item_line
334 && (i - list_line > 5 || blank_line_count > 1)
335 {
336 last_baseline = None;
337 last_list_item_line = None;
338 }
339 blank_line_count = 0;
340 continue;
341 }
342
343 if last_baseline.is_some() {
345 baselines[i] = last_baseline;
346 blank_line_count = 0;
347 }
348 }
349
350 baselines
351 }
352
353 fn is_indented_code_block_with_context(
355 &self,
356 lines: &[&str],
357 i: usize,
358 is_mkdocs: bool,
359 ctx: &IndentContext,
360 ) -> bool {
361 if i >= lines.len() {
362 return false;
363 }
364
365 let line = lines[i];
366
367 let indent = calculate_indentation_width_default(line);
369 if indent < 4 {
370 return false;
371 }
372
373 if ctx.in_list_context[i] {
379 let crosses_baseline = ctx
380 .list_item_baseline
381 .get(i)
382 .copied()
383 .flatten()
384 .is_some_and(|base| indent >= base + 4);
385 if !crosses_baseline {
386 return false;
387 }
388 }
389
390 if is_mkdocs && ctx.in_tab_context[i] {
392 return false;
393 }
394
395 if is_mkdocs && ctx.in_admonition_context[i] {
398 return false;
399 }
400
401 if ctx.in_comment_or_html.get(i).copied().unwrap_or(false) {
407 return false;
408 }
409
410 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
415 let prev_is_indented_code = i > 0
416 && {
417 let prev_indent = calculate_indentation_width_default(lines[i - 1]);
418 if prev_indent < 4 {
419 false
420 } else if ctx.in_list_context[i - 1] {
421 ctx.list_item_baseline
422 .get(i - 1)
423 .copied()
424 .flatten()
425 .is_some_and(|base| prev_indent >= base + 4)
426 } else {
427 true
428 }
429 }
430 && !(is_mkdocs && ctx.in_tab_context[i - 1])
431 && !(is_mkdocs && ctx.in_admonition_context[i - 1])
432 && !ctx.in_comment_or_html.get(i - 1).copied().unwrap_or(false);
433
434 if !has_blank_line_before && !prev_is_indented_code {
437 return false;
438 }
439
440 true
441 }
442
443 fn precompute_comment_or_html_context(ctx: &crate::lint_context::LintContext, line_count: usize) -> Vec<bool> {
452 (0..line_count)
453 .map(|i| {
454 ctx.line_info(i + 1).is_some_and(|info| {
455 info.in_html_comment
456 || info.in_mdx_comment
457 || info.in_html_block
458 || info.in_jsx_block
459 || info.in_mkdocstrings
460 || info.in_footnote_definition
461 || info.blockquote.is_some()
462 || info.in_front_matter
463 })
464 })
465 .collect()
466 }
467
468 fn precompute_mkdocs_tab_context(&self, lines: &[&str]) -> Vec<bool> {
470 let mut in_tab_context = vec![false; lines.len()];
471 let mut current_tab_indent: Option<usize> = None;
472
473 for (i, line) in lines.iter().enumerate() {
474 if mkdocs_tabs::is_tab_marker(line) {
476 let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
477 current_tab_indent = Some(tab_indent);
478 in_tab_context[i] = true;
479 continue;
480 }
481
482 if let Some(tab_indent) = current_tab_indent {
484 if mkdocs_tabs::is_tab_content(line, tab_indent) {
485 in_tab_context[i] = true;
486 } else if !line.trim().is_empty() && calculate_indentation_width_default(line) < 4 {
487 current_tab_indent = None;
489 } else {
490 in_tab_context[i] = true;
492 }
493 }
494 }
495
496 in_tab_context
497 }
498
499 fn precompute_mkdocs_admonition_context(&self, lines: &[&str]) -> Vec<bool> {
508 let mut in_admonition_context = vec![false; lines.len()];
509 let mut admonition_stack: Vec<usize> = Vec::new();
511
512 for (i, line) in lines.iter().enumerate() {
513 let line_indent = calculate_indentation_width_default(line);
514
515 if mkdocs_admonitions::is_admonition_start(line) {
517 let adm_indent = mkdocs_admonitions::get_admonition_indent(line).unwrap_or(0);
518
519 while let Some(&top_indent) = admonition_stack.last() {
521 if adm_indent <= top_indent {
523 admonition_stack.pop();
524 } else {
525 break;
526 }
527 }
528
529 admonition_stack.push(adm_indent);
531 in_admonition_context[i] = true;
532 continue;
533 }
534
535 if line.trim().is_empty() {
537 if !admonition_stack.is_empty() {
538 in_admonition_context[i] = true;
539 }
540 continue;
541 }
542
543 while let Some(&top_indent) = admonition_stack.last() {
546 if line_indent >= top_indent + 4 {
548 break;
550 } else {
551 admonition_stack.pop();
553 }
554 }
555
556 if !admonition_stack.is_empty() {
558 in_admonition_context[i] = true;
559 }
560 }
561
562 in_admonition_context
563 }
564
565 fn build_indent_context(
577 &self,
578 ctx: &crate::lint_context::LintContext,
579 lines: &[&str],
580 is_mkdocs: bool,
581 ) -> OwnedIndentContext {
582 OwnedIndentContext {
583 in_list_context: self.precompute_block_continuation_context(lines),
584 in_tab_context: if is_mkdocs {
585 self.precompute_mkdocs_tab_context(lines)
586 } else {
587 vec![false; lines.len()]
588 },
589 in_admonition_context: if is_mkdocs {
590 self.precompute_mkdocs_admonition_context(lines)
591 } else {
592 vec![false; lines.len()]
593 },
594 in_comment_or_html: Self::precompute_comment_or_html_context(ctx, lines.len()),
595 list_item_baseline: self.precompute_list_item_baseline(ctx, lines),
596 }
597 }
598
599 fn categorize_indented_blocks(
611 &self,
612 lines: &[&str],
613 is_mkdocs: bool,
614 ictx: &IndentContext<'_>,
615 ) -> (Vec<bool>, Vec<bool>) {
616 let mut is_misplaced = vec![false; lines.len()];
617 let mut contains_fences = vec![false; lines.len()];
618
619 let mut i = 0;
621 while i < lines.len() {
622 if !self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx) {
624 i += 1;
625 continue;
626 }
627
628 let block_start = i;
630 let mut block_end = i;
631
632 while block_end < lines.len() && self.is_indented_code_block_with_context(lines, block_end, is_mkdocs, ictx)
633 {
634 block_end += 1;
635 }
636
637 if block_end > block_start {
639 let first_line = lines[block_start].trim_start();
640 let last_line = lines[block_end - 1].trim_start();
641
642 let is_backtick_fence = first_line.starts_with("```");
644 let is_tilde_fence = first_line.starts_with("~~~");
645
646 if is_backtick_fence || is_tilde_fence {
647 let fence_char = if is_backtick_fence { '`' } else { '~' };
648 let opener_len = first_line.chars().take_while(|&c| c == fence_char).count();
649
650 let closer_fence_len = last_line.chars().take_while(|&c| c == fence_char).count();
652 let after_closer = &last_line[closer_fence_len..];
653
654 if closer_fence_len >= opener_len && after_closer.trim().is_empty() {
655 is_misplaced[block_start..block_end].fill(true);
657 } else {
658 contains_fences[block_start..block_end].fill(true);
660 }
661 } else {
662 let has_fence_markers = (block_start..block_end).any(|j| {
665 let trimmed = lines[j].trim_start();
666 trimmed.starts_with("```") || trimmed.starts_with("~~~")
667 });
668
669 if has_fence_markers {
670 contains_fences[block_start..block_end].fill(true);
671 }
672 }
673 }
674
675 i = block_end;
676 }
677
678 (is_misplaced, contains_fences)
679 }
680
681 fn check_unclosed_code_blocks(&self, ctx: &crate::lint_context::LintContext) -> Vec<LintWarning> {
682 let mut warnings = Vec::new();
683 let lines = ctx.raw_lines();
684
685 let has_markdown_doc_block = ctx.code_block_details.iter().any(|d| {
687 if !d.is_fenced {
688 return false;
689 }
690 let lang = d.info_string.to_lowercase();
691 lang.starts_with("markdown") || lang.starts_with("md")
692 });
693
694 if has_markdown_doc_block {
697 return warnings;
698 }
699
700 for detail in &ctx.code_block_details {
701 if !detail.is_fenced {
702 continue;
703 }
704
705 if detail.end != ctx.content.len() {
707 continue;
708 }
709
710 let opening_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
712 Ok(idx) => idx,
713 Err(idx) => idx.saturating_sub(1),
714 };
715
716 let line = lines.get(opening_line_idx).unwrap_or(&"");
718 let trimmed = line.trim();
719 let fence_marker = if let Some(pos) = trimmed.find("```") {
720 let count = trimmed[pos..].chars().take_while(|&c| c == '`').count();
721 "`".repeat(count)
722 } else if let Some(pos) = trimmed.find("~~~") {
723 let count = trimmed[pos..].chars().take_while(|&c| c == '~').count();
724 "~".repeat(count)
725 } else {
726 "```".to_string()
727 };
728
729 let last_non_empty_line = lines.iter().rev().find(|l| !l.trim().is_empty()).unwrap_or(&"");
731 let last_trimmed = last_non_empty_line.trim();
732 let fence_char = fence_marker.chars().next().unwrap_or('`');
733
734 let has_closing_fence = if fence_char == '`' {
735 last_trimmed.starts_with("```") && {
736 let fence_len = last_trimmed.chars().take_while(|&c| c == '`').count();
737 last_trimmed[fence_len..].trim().is_empty()
738 }
739 } else {
740 last_trimmed.starts_with("~~~") && {
741 let fence_len = last_trimmed.chars().take_while(|&c| c == '~').count();
742 last_trimmed[fence_len..].trim().is_empty()
743 }
744 };
745
746 if !has_closing_fence {
747 if ctx
749 .lines
750 .get(opening_line_idx)
751 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
752 {
753 continue;
754 }
755
756 let (start_line, start_col, end_line, end_col) = calculate_line_range(opening_line_idx + 1, line);
757
758 warnings.push(LintWarning {
759 rule_name: Some(self.name().to_string()),
760 line: start_line,
761 column: start_col,
762 end_line,
763 end_column: end_col,
764 message: format!("Code block opened with '{fence_marker}' but never closed"),
765 severity: Severity::Warning,
766 fix: Some(Fix::new(
767 ctx.content.len()..ctx.content.len(),
768 format!("\n{fence_marker}"),
769 )),
770 });
771 }
772 }
773
774 warnings
775 }
776
777 fn detect_style(
778 &self,
779 ctx: &crate::lint_context::LintContext,
780 lines: &[&str],
781 is_mkdocs: bool,
782 ictx: &IndentContext,
783 ) -> Option<CodeBlockStyle> {
784 if lines.is_empty() {
785 return None;
786 }
787
788 let mut fenced_count = 0;
789 let mut indented_count = 0;
790
791 let mut in_fenced = false;
801 let mut prev_was_indented = false;
802
803 for (i, line) in lines.iter().enumerate() {
804 let in_container = ictx.in_comment_or_html.get(i).copied().unwrap_or(false);
805
806 if ctx.flavor.supports_colon_code_fences() && ctx.lines.get(i).is_some_and(|l| l.in_code_block) {
810 prev_was_indented = false;
811 continue;
812 }
813
814 if ctx.flavor.supports_myst_directives() && ctx.lines.get(i).is_some_and(|l| l.in_myst_directive) {
816 prev_was_indented = false;
817 continue;
818 }
819
820 if self.is_fenced_code_block_start(line) {
821 if in_container {
822 prev_was_indented = false;
825 continue;
826 }
827 if !in_fenced {
828 fenced_count += 1;
830 in_fenced = true;
831 } else {
832 in_fenced = false;
834 }
835 prev_was_indented = false;
836 } else if !in_fenced && self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx) {
837 if !prev_was_indented {
839 indented_count += 1;
840 }
841 prev_was_indented = true;
842 } else {
843 prev_was_indented = false;
844 }
845 }
846
847 if fenced_count == 0 && indented_count == 0 {
848 None
849 } else if fenced_count > 0 && indented_count == 0 {
850 Some(CodeBlockStyle::Fenced)
851 } else if fenced_count == 0 && indented_count > 0 {
852 Some(CodeBlockStyle::Indented)
853 } else if fenced_count >= indented_count {
854 Some(CodeBlockStyle::Fenced)
855 } else {
856 Some(CodeBlockStyle::Indented)
857 }
858 }
859}
860
861impl Rule for MD046CodeBlockStyle {
862 fn name(&self) -> &'static str {
863 "MD046"
864 }
865
866 fn description(&self) -> &'static str {
867 "Code blocks should use a consistent style"
868 }
869
870 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
871 if ctx.content.is_empty() {
873 return Ok(Vec::new());
874 }
875
876 if !ctx.content.contains("```")
878 && !ctx.content.contains("~~~")
879 && !ctx.content.contains(" ")
880 && !ctx.content.contains('\t')
881 {
882 return Ok(Vec::new());
883 }
884
885 let unclosed_warnings = self.check_unclosed_code_blocks(ctx);
887
888 if !unclosed_warnings.is_empty() {
890 return Ok(unclosed_warnings);
891 }
892
893 let lines = ctx.raw_lines();
895 let mut warnings = Vec::new();
896
897 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
898
899 let target_style = match self.config.style {
901 CodeBlockStyle::Consistent => {
902 let owned = self.build_indent_context(ctx, lines, is_mkdocs);
903 self.detect_style(ctx, lines, is_mkdocs, &owned.borrow())
904 .unwrap_or(CodeBlockStyle::Fenced)
905 }
906 _ => self.config.style,
907 };
908
909 let mut reported_indented_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
911
912 for detail in &ctx.code_block_details {
913 if detail.start >= ctx.content.len() || detail.end > ctx.content.len() {
914 continue;
915 }
916
917 let start_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
918 Ok(idx) => idx,
919 Err(idx) => idx.saturating_sub(1),
920 };
921
922 if detail.is_fenced {
923 if target_style == CodeBlockStyle::Indented {
924 let line = lines.get(start_line_idx).unwrap_or(&"");
925
926 if ctx
927 .lines
928 .get(start_line_idx)
929 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment || info.in_footnote_definition)
930 {
931 continue;
932 }
933
934 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
935 warnings.push(LintWarning {
936 rule_name: Some(self.name().to_string()),
937 line: start_line,
938 column: start_col,
939 end_line,
940 end_column: end_col,
941 message: "Use indented code blocks".to_string(),
942 severity: Severity::Warning,
943 fix: None,
944 });
945 }
946 } else {
947 if target_style == CodeBlockStyle::Fenced && !reported_indented_lines.contains(&start_line_idx) {
949 let line = lines.get(start_line_idx).unwrap_or(&"");
950
951 if ctx.lines.get(start_line_idx).is_some_and(|info| {
953 info.in_html_comment
954 || info.in_mdx_comment
955 || info.in_html_block
956 || info.in_jsx_block
957 || info.in_mkdocstrings
958 || info.in_footnote_definition
959 || info.blockquote.is_some()
960 || info.in_front_matter
961 }) {
962 continue;
963 }
964
965 if is_mkdocs
967 && ctx
968 .lines
969 .get(start_line_idx)
970 .is_some_and(|info| info.in_admonition || info.in_content_tab)
971 {
972 continue;
973 }
974
975 reported_indented_lines.insert(start_line_idx);
976
977 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
978 warnings.push(LintWarning {
979 rule_name: Some(self.name().to_string()),
980 line: start_line,
981 column: start_col,
982 end_line,
983 end_column: end_col,
984 message: "Use fenced code blocks".to_string(),
985 severity: Severity::Warning,
986 fix: None,
987 });
988 }
989 }
990 }
991
992 warnings.sort_by_key(|w| (w.line, w.column));
994
995 Ok(warnings)
996 }
997
998 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
999 let content = ctx.content;
1000 if content.is_empty() {
1001 return Ok(String::new());
1002 }
1003
1004 let lines = ctx.raw_lines();
1005
1006 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
1008
1009 let owned = self.build_indent_context(ctx, lines, is_mkdocs);
1010 let ictx = owned.borrow();
1011
1012 let target_style = match self.config.style {
1013 CodeBlockStyle::Consistent => self
1014 .detect_style(ctx, lines, is_mkdocs, &ictx)
1015 .unwrap_or(CodeBlockStyle::Fenced),
1016 _ => self.config.style,
1017 };
1018
1019 let (misplaced_fence_lines, unsafe_fence_lines) = self.categorize_indented_blocks(lines, is_mkdocs, &ictx);
1023
1024 let mut result = String::with_capacity(content.len());
1025 let mut in_fenced_block = false;
1026 let mut fenced_fence_opener: Option<(char, usize)> = None;
1030 let mut in_indented_block = false;
1031 let mut current_block_fence_indent = String::new();
1036
1037 let mut current_block_disabled = false;
1039
1040 for (i, line) in lines.iter().enumerate() {
1041 let line_num = i + 1;
1042 let trimmed = line.trim_start();
1043
1044 if !in_fenced_block
1047 && Self::has_valid_fence_indent(line)
1048 && (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
1049 {
1050 current_block_disabled = ctx.inline_config().is_rule_disabled(self.name(), line_num);
1052 in_fenced_block = true;
1053 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
1054 let opener_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1055 fenced_fence_opener = Some((fence_char, opener_len));
1056
1057 if current_block_disabled {
1058 result.push_str(line);
1060 result.push('\n');
1061 } else if target_style == CodeBlockStyle::Indented {
1062 in_indented_block = true;
1064 } else {
1065 result.push_str(line);
1067 result.push('\n');
1068 }
1069 } else if in_fenced_block && fenced_fence_opener.is_some() {
1070 let (fence_char, opener_len) = fenced_fence_opener.unwrap();
1071 let closer_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1074 let after_closer = &trimmed[closer_len..];
1075 let is_closer = closer_len >= opener_len && after_closer.trim().is_empty() && closer_len > 0;
1076 if is_closer {
1077 in_fenced_block = false;
1078 fenced_fence_opener = None;
1079 in_indented_block = false;
1080
1081 if current_block_disabled {
1082 result.push_str(line);
1083 result.push('\n');
1084 } else if target_style == CodeBlockStyle::Indented {
1085 } else {
1087 result.push_str(line);
1089 result.push('\n');
1090 }
1091 current_block_disabled = false;
1092 } else if current_block_disabled {
1093 result.push_str(line);
1095 result.push('\n');
1096 } else if target_style == CodeBlockStyle::Indented {
1097 if !line.is_empty() {
1104 result.push_str(" ");
1105 result.push_str(line);
1106 }
1107 result.push('\n');
1108 } else {
1109 result.push_str(line);
1111 result.push('\n');
1112 }
1113 } else if self.is_indented_code_block_with_context(lines, i, is_mkdocs, &ictx) {
1114 if ctx.inline_config().is_rule_disabled(self.name(), line_num) {
1118 result.push_str(line);
1119 result.push('\n');
1120 continue;
1121 }
1122
1123 let prev_line_is_indented =
1125 i > 0 && self.is_indented_code_block_with_context(lines, i - 1, is_mkdocs, &ictx);
1126
1127 if target_style == CodeBlockStyle::Fenced {
1128 let baseline = ictx.list_item_baseline.get(i).copied().flatten().unwrap_or(0);
1134 let body = line.strip_prefix(" ").unwrap_or(line);
1140
1141 if misplaced_fence_lines[i] {
1144 result.push_str(line.trim_start());
1146 result.push('\n');
1147 } else if unsafe_fence_lines[i] {
1148 result.push_str(line);
1151 result.push('\n');
1152 } else if !prev_line_is_indented && !in_indented_block {
1153 current_block_fence_indent = " ".repeat(baseline);
1155 result.push_str(¤t_block_fence_indent);
1156 result.push_str("```\n");
1157 result.push_str(body);
1158 result.push('\n');
1159 in_indented_block = true;
1160 } else {
1161 result.push_str(body);
1163 result.push('\n');
1164 }
1165
1166 let next_line_is_indented =
1168 i < lines.len() - 1 && self.is_indented_code_block_with_context(lines, i + 1, is_mkdocs, &ictx);
1169 if !next_line_is_indented
1171 && in_indented_block
1172 && !misplaced_fence_lines[i]
1173 && !unsafe_fence_lines[i]
1174 {
1175 result.push_str(¤t_block_fence_indent);
1176 result.push_str("```\n");
1177 in_indented_block = false;
1178 current_block_fence_indent.clear();
1179 }
1180 } else {
1181 result.push_str(line);
1183 result.push('\n');
1184 }
1185 } else {
1186 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1188 result.push_str(¤t_block_fence_indent);
1189 result.push_str("```\n");
1190 in_indented_block = false;
1191 current_block_fence_indent.clear();
1192 }
1193
1194 result.push_str(line);
1195 result.push('\n');
1196 }
1197 }
1198
1199 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1201 result.push_str(¤t_block_fence_indent);
1202 result.push_str("```\n");
1203 }
1204
1205 if let Some((fence_char, opener_len)) = fenced_fence_opener
1211 && in_fenced_block
1212 {
1213 let has_unclosed_violation = !self.check_unclosed_code_blocks(ctx).is_empty();
1214 if has_unclosed_violation {
1215 let closer: String = std::iter::repeat_n(fence_char, opener_len).collect();
1216 result.push_str(&closer);
1217 result.push('\n');
1218 }
1219 }
1220
1221 if !content.ends_with('\n') && result.ends_with('\n') {
1223 result.pop();
1224 }
1225
1226 Ok(result)
1227 }
1228
1229 fn category(&self) -> RuleCategory {
1231 RuleCategory::CodeBlock
1232 }
1233
1234 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
1236 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
1239 }
1240
1241 fn as_any(&self) -> &dyn std::any::Any {
1242 self
1243 }
1244
1245 crate::impl_rule_config_methods!(MD046Config);
1246}
1247
1248#[cfg(test)]
1249mod tests {
1250 use super::*;
1251 use crate::lint_context::LintContext;
1252
1253 fn detect_style_from_content(rule: &MD046CodeBlockStyle, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
1265 let flavor = if is_mkdocs {
1266 crate::config::MarkdownFlavor::MkDocs
1267 } else {
1268 crate::config::MarkdownFlavor::Standard
1269 };
1270 let ctx = LintContext::new(content, flavor, None);
1271 let lines: Vec<&str> = content.lines().collect();
1272 let in_list_context = rule.precompute_block_continuation_context(&lines);
1273 let in_tab_context = if is_mkdocs {
1274 rule.precompute_mkdocs_tab_context(&lines)
1275 } else {
1276 vec![false; lines.len()]
1277 };
1278 let in_admonition_context = if is_mkdocs {
1279 rule.precompute_mkdocs_admonition_context(&lines)
1280 } else {
1281 vec![false; lines.len()]
1282 };
1283 let in_comment_or_html = vec![false; lines.len()];
1284 let list_item_baseline: Vec<Option<usize>> = vec![None; lines.len()];
1290 let ictx = IndentContext {
1291 in_list_context: &in_list_context,
1292 in_tab_context: &in_tab_context,
1293 in_admonition_context: &in_admonition_context,
1294 in_comment_or_html: &in_comment_or_html,
1295 list_item_baseline: &list_item_baseline,
1296 };
1297 rule.detect_style(&ctx, &lines, is_mkdocs, &ictx)
1298 }
1299
1300 #[test]
1301 fn test_fenced_code_block_detection() {
1302 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1303 assert!(rule.is_fenced_code_block_start("```"));
1304 assert!(rule.is_fenced_code_block_start("```rust"));
1305 assert!(rule.is_fenced_code_block_start("~~~"));
1306 assert!(rule.is_fenced_code_block_start("~~~python"));
1307 assert!(rule.is_fenced_code_block_start(" ```"));
1308 assert!(!rule.is_fenced_code_block_start("``"));
1309 assert!(!rule.is_fenced_code_block_start("~~"));
1310 assert!(!rule.is_fenced_code_block_start("Regular text"));
1311 }
1312
1313 #[test]
1314 fn test_consistent_style_with_fenced_blocks() {
1315 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1316 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
1317 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1318 let result = rule.check(&ctx).unwrap();
1319
1320 assert_eq!(result.len(), 0);
1322 }
1323
1324 #[test]
1325 fn test_consistent_style_with_indented_blocks() {
1326 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1327 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
1328 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1329 let result = rule.check(&ctx).unwrap();
1330
1331 assert_eq!(result.len(), 0);
1333 }
1334
1335 #[test]
1336 fn test_consistent_style_mixed() {
1337 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1338 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
1339 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1340 let result = rule.check(&ctx).unwrap();
1341
1342 assert!(!result.is_empty());
1344 }
1345
1346 #[test]
1347 fn test_fenced_style_with_indented_blocks() {
1348 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1349 let content = "Text\n\n indented code\n more code\n\nMore text";
1350 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1351 let result = rule.check(&ctx).unwrap();
1352
1353 assert!(!result.is_empty());
1355 assert!(result[0].message.contains("Use fenced code blocks"));
1356 }
1357
1358 #[test]
1359 fn test_fenced_style_with_tab_indented_blocks() {
1360 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1361 let content = "Text\n\n\ttab indented code\n\tmore code\n\nMore text";
1362 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1363 let result = rule.check(&ctx).unwrap();
1364
1365 assert!(!result.is_empty());
1367 assert!(result[0].message.contains("Use fenced code blocks"));
1368 }
1369
1370 #[test]
1371 fn test_fenced_style_with_mixed_whitespace_indented_blocks() {
1372 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1373 let content = "Text\n\n \tmixed indent code\n \tmore code\n\nMore text";
1375 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1376 let result = rule.check(&ctx).unwrap();
1377
1378 assert!(
1380 !result.is_empty(),
1381 "Mixed whitespace (2 spaces + tab) should be detected as indented code"
1382 );
1383 assert!(result[0].message.contains("Use fenced code blocks"));
1384 }
1385
1386 #[test]
1387 fn test_fenced_style_with_one_space_tab_indent() {
1388 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1389 let content = "Text\n\n \ttab after one space\n \tmore code\n\nMore text";
1391 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1392 let result = rule.check(&ctx).unwrap();
1393
1394 assert!(!result.is_empty(), "1 space + tab should be detected as indented code");
1395 assert!(result[0].message.contains("Use fenced code blocks"));
1396 }
1397
1398 #[test]
1399 fn test_indented_style_with_fenced_blocks() {
1400 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1401 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
1402 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1403 let result = rule.check(&ctx).unwrap();
1404
1405 assert!(!result.is_empty());
1407 assert!(result[0].message.contains("Use indented code blocks"));
1408 }
1409
1410 #[test]
1411 fn test_unclosed_code_block() {
1412 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1413 let content = "```\ncode without closing fence";
1414 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1415 let result = rule.check(&ctx).unwrap();
1416
1417 assert_eq!(result.len(), 1);
1418 assert!(result[0].message.contains("never closed"));
1419 }
1420
1421 #[test]
1422 fn test_nested_code_blocks() {
1423 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1424 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
1425 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1426 let result = rule.check(&ctx).unwrap();
1427
1428 assert_eq!(result.len(), 0);
1430 }
1431
1432 #[test]
1433 fn test_fix_indented_to_fenced() {
1434 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1435 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
1436 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1437 let fixed = rule.fix(&ctx).unwrap();
1438
1439 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
1440 }
1441
1442 #[test]
1443 fn test_fix_fenced_to_indented() {
1444 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1445 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
1446 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1447 let fixed = rule.fix(&ctx).unwrap();
1448
1449 assert!(fixed.contains(" code line 1\n code line 2"));
1450 assert!(!fixed.contains("```"));
1451 }
1452
1453 #[test]
1454 fn test_fix_fenced_to_indented_blank_lines_have_no_trailing_spaces() {
1455 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1459 let content = "Text\n\n```\ncode line 1\n\ncode line 2\n```\n\nMore text";
1460 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1461 let fixed = rule.fix(&ctx).unwrap();
1462
1463 for line in fixed.lines() {
1464 assert!(
1465 line.is_empty() || !line.trim_end().is_empty() || line == line.trim_end(),
1466 "no line may have trailing whitespace, got {line:?}"
1467 );
1468 assert_ne!(line, " ", "blank line was indented to trailing spaces");
1469 }
1470 assert!(fixed.contains(" code line 1\n\n code line 2"));
1472 }
1473
1474 #[test]
1475 fn test_is_list_item_requires_delimiter_after_digits() {
1476 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1477 assert!(rule.is_list_item("1. First"));
1479 assert!(rule.is_list_item("42) Item"));
1480 assert!(rule.is_list_item(" 3. Indented item"));
1481 assert!(rule.is_list_item("- bullet"));
1483 assert!(rule.is_list_item("* bullet"));
1484 assert!(!rule.is_list_item("2 results. More info."));
1487 assert!(!rule.is_list_item("3 options (a, b) here"));
1488 assert!(!rule.is_list_item("100 items in stock. Buy now"));
1489 }
1490
1491 #[test]
1492 fn test_fix_fenced_to_indented_preserves_internal_indentation() {
1493 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1496 let content = r#"# Test
1497
1498```html
1499<!doctype html>
1500<html>
1501 <head>
1502 <title>Test</title>
1503 </head>
1504</html>
1505```
1506"#;
1507 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1508 let fixed = rule.fix(&ctx).unwrap();
1509
1510 assert!(
1513 fixed.contains(" <head>"),
1514 "Expected 6 spaces before <head> (4 for code block + 2 original), got:\n{fixed}"
1515 );
1516 assert!(
1517 fixed.contains(" <title>"),
1518 "Expected 8 spaces before <title> (4 for code block + 4 original), got:\n{fixed}"
1519 );
1520 assert!(!fixed.contains("```"), "Fenced markers should be removed");
1521 }
1522
1523 #[test]
1524 fn test_fix_fenced_to_indented_preserves_python_indentation() {
1525 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1527 let content = r#"# Python Example
1528
1529```python
1530def greet(name):
1531 if name:
1532 print(f"Hello, {name}!")
1533 else:
1534 print("Hello, World!")
1535```
1536"#;
1537 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1538 let fixed = rule.fix(&ctx).unwrap();
1539
1540 assert!(
1542 fixed.contains(" def greet(name):"),
1543 "Function def should have 4 spaces (code block indent)"
1544 );
1545 assert!(
1546 fixed.contains(" if name:"),
1547 "if statement should have 8 spaces (4 code + 4 Python)"
1548 );
1549 assert!(
1550 fixed.contains(" print"),
1551 "print should have 12 spaces (4 code + 8 Python)"
1552 );
1553 }
1554
1555 #[test]
1556 fn test_fix_fenced_to_indented_preserves_yaml_indentation() {
1557 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1559 let content = r#"# Config
1560
1561```yaml
1562server:
1563 host: localhost
1564 port: 8080
1565 ssl:
1566 enabled: true
1567 cert: /path/to/cert
1568```
1569"#;
1570 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1571 let fixed = rule.fix(&ctx).unwrap();
1572
1573 assert!(fixed.contains(" server:"), "Root key should have 4 spaces");
1574 assert!(fixed.contains(" host:"), "First level should have 6 spaces");
1575 assert!(fixed.contains(" ssl:"), "ssl key should have 6 spaces");
1576 assert!(fixed.contains(" enabled:"), "Nested ssl should have 8 spaces");
1577 }
1578
1579 #[test]
1580 fn test_fix_fenced_to_indented_preserves_empty_lines() {
1581 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1585 let content = "```\nline1\n\nline2\n```\n";
1586 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1587 let fixed = rule.fix(&ctx).unwrap();
1588
1589 assert!(fixed.contains(" line1"), "line1 should be indented");
1591 assert!(fixed.contains(" line2"), "line2 should be indented");
1592 assert!(
1593 fixed.contains(" line1\n\n line2"),
1594 "blank line must stay empty, got {fixed:?}"
1595 );
1596 }
1597
1598 #[test]
1599 fn test_fix_fenced_to_indented_multiple_blocks() {
1600 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1602 let content = r#"# Doc
1603
1604```python
1605def foo():
1606 pass
1607```
1608
1609Text between.
1610
1611```yaml
1612key:
1613 value: 1
1614```
1615"#;
1616 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1617 let fixed = rule.fix(&ctx).unwrap();
1618
1619 assert!(fixed.contains(" def foo():"), "Python def should be indented");
1620 assert!(fixed.contains(" pass"), "Python body should have 8 spaces");
1621 assert!(fixed.contains(" key:"), "YAML root should have 4 spaces");
1622 assert!(fixed.contains(" value:"), "YAML nested should have 6 spaces");
1623 assert!(!fixed.contains("```"), "No fence markers should remain");
1624 }
1625
1626 #[test]
1627 fn test_fix_unclosed_block() {
1628 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1629 let content = "```\ncode without closing";
1630 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1631 let fixed = rule.fix(&ctx).unwrap();
1632
1633 assert!(fixed.ends_with("```"));
1635 }
1636
1637 #[test]
1638 fn test_code_block_in_list() {
1639 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1640 let content = "- List item\n code in list\n more code\n- Next item";
1641 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1642 let result = rule.check(&ctx).unwrap();
1643
1644 assert_eq!(result.len(), 0);
1646 }
1647
1648 #[test]
1649 fn test_detect_style_fenced() {
1650 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1651 let content = "```\ncode\n```";
1652 let style = detect_style_from_content(&rule, content, false);
1653
1654 assert_eq!(style, Some(CodeBlockStyle::Fenced));
1655 }
1656
1657 #[test]
1658 fn test_detect_style_indented() {
1659 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1660 let content = "Text\n\n code\n\nMore";
1661 let style = detect_style_from_content(&rule, content, false);
1662
1663 assert_eq!(style, Some(CodeBlockStyle::Indented));
1664 }
1665
1666 #[test]
1667 fn test_detect_style_none() {
1668 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1669 let content = "No code blocks here";
1670 let style = detect_style_from_content(&rule, content, false);
1671
1672 assert_eq!(style, None);
1673 }
1674
1675 #[test]
1676 fn test_tilde_fence() {
1677 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1678 let content = "~~~\ncode\n~~~";
1679 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1680 let result = rule.check(&ctx).unwrap();
1681
1682 assert_eq!(result.len(), 0);
1684 }
1685
1686 #[test]
1687 fn test_language_specification() {
1688 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1689 let content = "```rust\nfn main() {}\n```";
1690 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1691 let result = rule.check(&ctx).unwrap();
1692
1693 assert_eq!(result.len(), 0);
1694 }
1695
1696 #[test]
1697 fn test_empty_content() {
1698 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1699 let content = "";
1700 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1701 let result = rule.check(&ctx).unwrap();
1702
1703 assert_eq!(result.len(), 0);
1704 }
1705
1706 #[test]
1707 fn test_default_config() {
1708 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1709 let (name, _config) = rule.default_config_section().unwrap();
1710 assert_eq!(name, "MD046");
1711 }
1712
1713 #[test]
1714 fn test_markdown_documentation_block() {
1715 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1716 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
1717 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1718 let result = rule.check(&ctx).unwrap();
1719
1720 assert_eq!(result.len(), 0);
1722 }
1723
1724 #[test]
1725 fn test_preserve_trailing_newline() {
1726 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1727 let content = "```\ncode\n```\n";
1728 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1729 let fixed = rule.fix(&ctx).unwrap();
1730
1731 assert_eq!(fixed, content);
1732 }
1733
1734 #[test]
1735 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
1736 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1737 let content = r#"# Document
1738
1739=== "Python"
1740
1741 This is tab content
1742 Not an indented code block
1743
1744 ```python
1745 def hello():
1746 print("Hello")
1747 ```
1748
1749=== "JavaScript"
1750
1751 More tab content here
1752 Also not an indented code block"#;
1753
1754 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1755 let result = rule.check(&ctx).unwrap();
1756
1757 assert_eq!(result.len(), 0);
1759 }
1760
1761 #[test]
1762 fn test_mkdocs_tabs_with_actual_indented_code() {
1763 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1764 let content = r#"# Document
1765
1766=== "Tab 1"
1767
1768 This is tab content
1769
1770Regular text
1771
1772 This is an actual indented code block
1773 Should be flagged"#;
1774
1775 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1776 let result = rule.check(&ctx).unwrap();
1777
1778 assert_eq!(result.len(), 1);
1780 assert!(result[0].message.contains("Use fenced code blocks"));
1781 }
1782
1783 #[test]
1784 fn test_mkdocs_tabs_detect_style() {
1785 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1786 let content = r#"=== "Tab 1"
1787
1788 Content in tab
1789 More content
1790
1791=== "Tab 2"
1792
1793 Content in second tab"#;
1794
1795 let style = detect_style_from_content(&rule, content, true);
1797 assert_eq!(style, None); let style = detect_style_from_content(&rule, content, false);
1801 assert_eq!(style, Some(CodeBlockStyle::Indented));
1802 }
1803
1804 #[test]
1805 fn test_mkdocs_nested_tabs() {
1806 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1807 let content = r#"# Document
1808
1809=== "Outer Tab"
1810
1811 Some content
1812
1813 === "Nested Tab"
1814
1815 Nested tab content
1816 Should not be flagged"#;
1817
1818 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1819 let result = rule.check(&ctx).unwrap();
1820
1821 assert_eq!(result.len(), 0);
1823 }
1824
1825 #[test]
1826 fn test_mkdocs_admonitions_not_flagged_as_indented_code() {
1827 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1830 let content = r#"# Document
1831
1832!!! note
1833 This is normal admonition content, not a code block.
1834 It spans multiple lines.
1835
1836??? warning "Collapsible Warning"
1837 This is also admonition content.
1838
1839???+ tip "Expanded Tip"
1840 And this one too.
1841
1842Regular text outside admonitions."#;
1843
1844 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1845 let result = rule.check(&ctx).unwrap();
1846
1847 assert_eq!(
1849 result.len(),
1850 0,
1851 "Admonition content in MkDocs mode should not trigger MD046"
1852 );
1853 }
1854
1855 #[test]
1856 fn test_mkdocs_admonition_with_actual_indented_code() {
1857 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1859 let content = r#"# Document
1860
1861!!! note
1862 This is admonition content.
1863
1864Regular text ends the admonition.
1865
1866 This is actual indented code (should be flagged)"#;
1867
1868 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1869 let result = rule.check(&ctx).unwrap();
1870
1871 assert_eq!(result.len(), 1);
1873 assert!(result[0].message.contains("Use fenced code blocks"));
1874 }
1875
1876 #[test]
1877 fn test_admonition_in_standard_mode_flagged() {
1878 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1882 let content = r#"# Document
1883
1884!!! note
1885
1886 This looks like code in standard mode.
1887
1888Regular text."#;
1889
1890 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1892 let result = rule.check(&ctx).unwrap();
1893
1894 assert_eq!(
1896 result.len(),
1897 1,
1898 "Admonition content in Standard mode should be flagged as indented code"
1899 );
1900 }
1901
1902 #[test]
1903 fn test_mkdocs_admonition_with_fenced_code_inside() {
1904 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1906 let content = r#"# Document
1907
1908!!! note "Code Example"
1909 Here's some code:
1910
1911 ```python
1912 def hello():
1913 print("world")
1914 ```
1915
1916 More text after code.
1917
1918Regular text."#;
1919
1920 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1921 let result = rule.check(&ctx).unwrap();
1922
1923 assert_eq!(result.len(), 0, "Fenced code blocks inside admonitions should be valid");
1925 }
1926
1927 #[test]
1928 fn test_mkdocs_nested_admonitions() {
1929 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1931 let content = r#"# Document
1932
1933!!! note "Outer"
1934 Outer content.
1935
1936 !!! warning "Inner"
1937 Inner content.
1938 More inner content.
1939
1940 Back to outer.
1941
1942Regular text."#;
1943
1944 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1945 let result = rule.check(&ctx).unwrap();
1946
1947 assert_eq!(result.len(), 0, "Nested admonitions should not be flagged");
1949 }
1950
1951 #[test]
1952 fn test_mkdocs_admonition_fix_does_not_wrap() {
1953 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1955 let content = r#"!!! note
1956 Content that should stay as admonition content.
1957 Not be wrapped in code fences.
1958"#;
1959
1960 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1961 let fixed = rule.fix(&ctx).unwrap();
1962
1963 assert!(
1965 !fixed.contains("```\n Content"),
1966 "Admonition content should not be wrapped in fences"
1967 );
1968 assert_eq!(fixed, content, "Content should remain unchanged");
1969 }
1970
1971 #[test]
1972 fn test_mkdocs_empty_admonition() {
1973 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1975 let content = r#"!!! note
1976
1977Regular paragraph after empty admonition.
1978
1979 This IS an indented code block (after blank + non-indented line)."#;
1980
1981 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1982 let result = rule.check(&ctx).unwrap();
1983
1984 assert_eq!(result.len(), 1, "Indented code after admonition ends should be flagged");
1986 }
1987
1988 #[test]
1989 fn test_mkdocs_indented_admonition() {
1990 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1992 let content = r#"- List item
1993
1994 !!! note
1995 Indented admonition content.
1996 More content.
1997
1998- Next item"#;
1999
2000 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2001 let result = rule.check(&ctx).unwrap();
2002
2003 assert_eq!(
2005 result.len(),
2006 0,
2007 "Indented admonitions (e.g., in lists) should not be flagged"
2008 );
2009 }
2010
2011 #[test]
2012 fn test_footnote_indented_paragraphs_not_flagged() {
2013 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2014 let content = r#"# Test Document with Footnotes
2015
2016This is some text with a footnote[^1].
2017
2018Here's some code:
2019
2020```bash
2021echo "fenced code block"
2022```
2023
2024More text with another footnote[^2].
2025
2026[^1]: Really interesting footnote text.
2027
2028 Even more interesting second paragraph.
2029
2030[^2]: Another footnote.
2031
2032 With a second paragraph too.
2033
2034 And even a third paragraph!"#;
2035
2036 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2037 let result = rule.check(&ctx).unwrap();
2038
2039 assert_eq!(result.len(), 0);
2041 }
2042
2043 #[test]
2044 fn test_footnote_definition_detection() {
2045 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2046
2047 assert!(rule.is_footnote_definition("[^1]: Footnote text"));
2050 assert!(rule.is_footnote_definition("[^foo]: Footnote text"));
2051 assert!(rule.is_footnote_definition("[^long-name]: Footnote text"));
2052 assert!(rule.is_footnote_definition("[^test_123]: Mixed chars"));
2053 assert!(rule.is_footnote_definition(" [^1]: Indented footnote"));
2054 assert!(rule.is_footnote_definition("[^a]: Minimal valid footnote"));
2055 assert!(rule.is_footnote_definition("[^123]: Numeric label"));
2056 assert!(rule.is_footnote_definition("[^_]: Single underscore"));
2057 assert!(rule.is_footnote_definition("[^-]: Single hyphen"));
2058
2059 assert!(!rule.is_footnote_definition("[^]: No label"));
2061 assert!(!rule.is_footnote_definition("[^ ]: Whitespace only"));
2062 assert!(!rule.is_footnote_definition("[^ ]: Multiple spaces"));
2063 assert!(!rule.is_footnote_definition("[^\t]: Tab only"));
2064
2065 assert!(!rule.is_footnote_definition("[^]]: Extra bracket"));
2067 assert!(!rule.is_footnote_definition("Regular text [^1]:"));
2068 assert!(!rule.is_footnote_definition("[1]: Not a footnote"));
2069 assert!(!rule.is_footnote_definition("[^")); assert!(!rule.is_footnote_definition("[^1:")); assert!(!rule.is_footnote_definition("^1]: Missing opening bracket"));
2072
2073 assert!(!rule.is_footnote_definition("[^test.name]: Period"));
2075 assert!(!rule.is_footnote_definition("[^test name]: Space in label"));
2076 assert!(!rule.is_footnote_definition("[^test@name]: Special char"));
2077 assert!(!rule.is_footnote_definition("[^test/name]: Slash"));
2078 assert!(!rule.is_footnote_definition("[^test\\name]: Backslash"));
2079
2080 assert!(!rule.is_footnote_definition("[^test\r]: Carriage return"));
2083 }
2084
2085 #[test]
2086 fn test_footnote_with_blank_lines() {
2087 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2091 let content = r#"# Document
2092
2093Text with footnote[^1].
2094
2095[^1]: First paragraph.
2096
2097 Second paragraph after blank line.
2098
2099 Third paragraph after another blank line.
2100
2101Regular text at column 0 ends the footnote."#;
2102
2103 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2104 let result = rule.check(&ctx).unwrap();
2105
2106 assert_eq!(
2108 result.len(),
2109 0,
2110 "Indented content within footnotes should not trigger MD046"
2111 );
2112 }
2113
2114 #[test]
2115 fn test_footnote_multiple_consecutive_blank_lines() {
2116 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2119 let content = r#"Text[^1].
2120
2121[^1]: First paragraph.
2122
2123
2124
2125 Content after three blank lines (still part of footnote).
2126
2127Not indented, so footnote ends here."#;
2128
2129 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2130 let result = rule.check(&ctx).unwrap();
2131
2132 assert_eq!(
2134 result.len(),
2135 0,
2136 "Multiple blank lines shouldn't break footnote continuation"
2137 );
2138 }
2139
2140 #[test]
2141 fn test_footnote_terminated_by_non_indented_content() {
2142 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2145 let content = r#"[^1]: Footnote content.
2146
2147 More indented content in footnote.
2148
2149This paragraph is not indented, so footnote ends.
2150
2151 This should be flagged as indented code block."#;
2152
2153 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2154 let result = rule.check(&ctx).unwrap();
2155
2156 assert_eq!(
2158 result.len(),
2159 1,
2160 "Indented code after footnote termination should be flagged"
2161 );
2162 assert!(
2163 result[0].message.contains("Use fenced code blocks"),
2164 "Expected MD046 warning for indented code block"
2165 );
2166 assert!(result[0].line >= 7, "Warning should be on the indented code block line");
2167 }
2168
2169 #[test]
2170 fn test_footnote_terminated_by_structural_elements() {
2171 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2173 let content = r#"[^1]: Footnote content.
2174
2175 More content.
2176
2177## Heading terminates footnote
2178
2179 This indented content should be flagged.
2180
2181---
2182
2183 This should also be flagged (after horizontal rule)."#;
2184
2185 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2186 let result = rule.check(&ctx).unwrap();
2187
2188 assert_eq!(
2190 result.len(),
2191 2,
2192 "Both indented blocks after termination should be flagged"
2193 );
2194 }
2195
2196 #[test]
2197 fn test_footnote_with_code_block_inside() {
2198 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2201 let content = r#"Text[^1].
2202
2203[^1]: Footnote with code:
2204
2205 ```python
2206 def hello():
2207 print("world")
2208 ```
2209
2210 More footnote text after code."#;
2211
2212 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2213 let result = rule.check(&ctx).unwrap();
2214
2215 assert_eq!(result.len(), 0, "Fenced code blocks within footnotes should be allowed");
2217 }
2218
2219 #[test]
2220 fn test_footnote_with_8_space_indented_code() {
2221 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2224 let content = r#"Text[^1].
2225
2226[^1]: Footnote with nested code.
2227
2228 code block
2229 more code"#;
2230
2231 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2232 let result = rule.check(&ctx).unwrap();
2233
2234 assert_eq!(
2236 result.len(),
2237 0,
2238 "8-space indented code within footnotes represents nested code blocks"
2239 );
2240 }
2241
2242 #[test]
2243 fn test_multiple_footnotes() {
2244 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2247 let content = r#"Text[^1] and more[^2].
2248
2249[^1]: First footnote.
2250
2251 Continuation of first.
2252
2253[^2]: Second footnote starts here, ending the first.
2254
2255 Continuation of second."#;
2256
2257 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2258 let result = rule.check(&ctx).unwrap();
2259
2260 assert_eq!(
2262 result.len(),
2263 0,
2264 "Multiple footnotes should each maintain their continuation context"
2265 );
2266 }
2267
2268 #[test]
2269 fn test_list_item_ends_footnote_context() {
2270 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2272 let content = r#"[^1]: Footnote.
2273
2274 Content in footnote.
2275
2276- List item starts here (ends footnote context).
2277
2278 This indented content is part of the list, not the footnote."#;
2279
2280 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2281 let result = rule.check(&ctx).unwrap();
2282
2283 assert_eq!(
2285 result.len(),
2286 0,
2287 "List items should end footnote context and start their own"
2288 );
2289 }
2290
2291 #[test]
2292 fn test_footnote_vs_actual_indented_code() {
2293 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2296 let content = r#"# Heading
2297
2298Text with footnote[^1].
2299
2300[^1]: Footnote content.
2301
2302 Part of footnote (should not be flagged).
2303
2304Regular paragraph ends footnote context.
2305
2306 This is actual indented code (MUST be flagged)
2307 Should be detected as code block"#;
2308
2309 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2310 let result = rule.check(&ctx).unwrap();
2311
2312 assert_eq!(
2314 result.len(),
2315 1,
2316 "Must still detect indented code blocks outside footnotes"
2317 );
2318 assert!(
2319 result[0].message.contains("Use fenced code blocks"),
2320 "Expected MD046 warning for indented code"
2321 );
2322 assert!(
2323 result[0].line >= 11,
2324 "Warning should be on the actual indented code line"
2325 );
2326 }
2327
2328 #[test]
2329 fn test_spec_compliant_label_characters() {
2330 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2333
2334 assert!(rule.is_footnote_definition("[^test]: text"));
2336 assert!(rule.is_footnote_definition("[^TEST]: text"));
2337 assert!(rule.is_footnote_definition("[^test-name]: text"));
2338 assert!(rule.is_footnote_definition("[^test_name]: text"));
2339 assert!(rule.is_footnote_definition("[^test123]: text"));
2340 assert!(rule.is_footnote_definition("[^123]: text"));
2341 assert!(rule.is_footnote_definition("[^a1b2c3]: text"));
2342
2343 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")); }
2351
2352 #[test]
2353 fn test_code_block_inside_html_comment() {
2354 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2357 let content = r#"# Document
2358
2359Some text.
2360
2361<!--
2362Example code block in comment:
2363
2364```typescript
2365console.log("Hello");
2366```
2367
2368More comment text.
2369-->
2370
2371More content."#;
2372
2373 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2374 let result = rule.check(&ctx).unwrap();
2375
2376 assert_eq!(
2377 result.len(),
2378 0,
2379 "Code blocks inside HTML comments should not be flagged as unclosed"
2380 );
2381 }
2382
2383 #[test]
2384 fn test_unclosed_fence_inside_html_comment() {
2385 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2387 let content = r#"# Document
2388
2389<!--
2390Example with intentionally unclosed fence:
2391
2392```
2393code without closing
2394-->
2395
2396More content."#;
2397
2398 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2399 let result = rule.check(&ctx).unwrap();
2400
2401 assert_eq!(
2402 result.len(),
2403 0,
2404 "Unclosed fences inside HTML comments should be ignored"
2405 );
2406 }
2407
2408 #[test]
2409 fn test_multiline_html_comment_with_indented_code() {
2410 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2412 let content = r#"# Document
2413
2414<!--
2415Example:
2416
2417 indented code
2418 more code
2419
2420End of comment.
2421-->
2422
2423Regular text."#;
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.len(),
2430 0,
2431 "Indented code inside HTML comments should not be flagged"
2432 );
2433 }
2434
2435 #[test]
2436 fn test_code_block_after_html_comment() {
2437 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2439 let content = r#"# Document
2440
2441<!-- comment -->
2442
2443Text before.
2444
2445 indented code should be flagged
2446
2447More text."#;
2448
2449 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2450 let result = rule.check(&ctx).unwrap();
2451
2452 assert_eq!(
2453 result.len(),
2454 1,
2455 "Code blocks after HTML comments should still be detected"
2456 );
2457 assert!(result[0].message.contains("Use fenced code blocks"));
2458 }
2459
2460 #[test]
2461 fn test_consistent_style_indented_html_comment() {
2462 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2468 let content = "# MD046 false-positive reproduction\n\
2469 \n\
2470 <!--\n \
2471 This is just an indented comment, not a code block.\n\
2472 \n \
2473 A second line is required to trigger the false-positive.\n\
2474 \n \
2475 Actually, three lines are required.\n\
2476 -->\n\
2477 \n\
2478 ```md\n\
2479 This should be fine, since it's the only code block and therefore consistent.\n\
2480 ```\n";
2481
2482 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2483 let result = rule.check(&ctx).unwrap();
2484
2485 assert_eq!(
2486 result,
2487 vec![],
2488 "A single fenced block and an indented HTML comment must produce no MD046 warnings",
2489 );
2490 }
2491
2492 #[test]
2493 fn test_consistent_style_indented_html_block() {
2494 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2501 let content = "# Heading\n\
2502 \n\
2503 <div class=\"note\">\n \
2504 line one of indented html content\n \
2505 line two of indented html content\n \
2506 line three of indented html content\n\
2507 </div>\n\
2508 \n\
2509 ```md\n\
2510 real fenced block\n\
2511 ```\n";
2512
2513 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2514 let result = rule.check(&ctx).unwrap();
2515
2516 assert_eq!(
2517 result,
2518 vec![],
2519 "Indented content inside a raw HTML block must not influence MD046 style detection",
2520 );
2521 }
2522
2523 #[test]
2524 fn test_consistent_style_fake_fence_inside_html_comment() {
2525 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2531 let content = "# Title\n\
2532 \n\
2533 <!--\n\
2534 ```\n\
2535 fake fence inside comment\n\
2536 ```\n\
2537 -->\n\
2538 \n \
2539 real indented code block line 1\n \
2540 real indented code block line 2\n";
2541
2542 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2543 let result = rule.check(&ctx).unwrap();
2544
2545 assert_eq!(
2546 result,
2547 vec![],
2548 "Fence markers inside an HTML comment must not influence MD046 style detection",
2549 );
2550 }
2551
2552 #[test]
2553 fn test_consistent_style_indented_footnote_definition() {
2554 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2558 let content = "# Heading\n\
2559 \n\
2560 Reference to a footnote[^note].\n\
2561 \n\
2562 [^note]: First line of the footnote.\n \
2563 Second indented continuation line.\n \
2564 Third indented continuation line.\n \
2565 Fourth indented continuation line.\n\
2566 \n\
2567 ```md\n\
2568 real fenced block\n\
2569 ```\n";
2570
2571 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2572 let result = rule.check(&ctx).unwrap();
2573
2574 assert_eq!(
2575 result,
2576 vec![],
2577 "Footnote-definition continuation content must not influence MD046 style detection",
2578 );
2579 }
2580
2581 #[test]
2582 fn test_consistent_style_indented_blockquote() {
2583 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2588 let content = "# Heading\n\
2589 \n\
2590 > line one of quoted indented content\n\
2591 >\n\
2592 > line two of quoted indented content\n\
2593 >\n\
2594 > line three of quoted indented content\n\
2595 \n\
2596 ```md\n\
2597 real fenced block\n\
2598 ```\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 "Indented content inside a blockquote must not influence MD046 style detection",
2607 );
2608 }
2609
2610 #[test]
2611 fn test_consistent_style_genuine_indented_block_detected_as_indented() {
2612 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2617 let content = "# Heading\n\
2618 \n\
2619 Some prose.\n\
2620 \n \
2621 real indented code line 1\n \
2622 real indented code line 2\n";
2623
2624 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2625 let result = rule.check(&ctx).unwrap();
2626
2627 assert_eq!(
2630 result,
2631 vec![],
2632 "A genuine top-level indented block must be detected as Indented style under Consistent",
2633 );
2634 }
2635
2636 #[test]
2637 fn test_consistent_style_skipped_lines_dont_override_real_block() {
2638 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2643 let content = "# Heading\n\
2644 \n\
2645 <!--\n \
2646 skipped indented comment line 1\n \
2647 skipped indented comment line 2\n\
2648 -->\n\
2649 \n\
2650 <!--\n \
2651 second skipped region\n \
2652 also skipped\n\
2653 -->\n\
2654 \n \
2655 real indented code line\n";
2656
2657 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2658 let result = rule.check(&ctx).unwrap();
2659
2660 assert_eq!(
2661 result,
2662 vec![],
2663 "Skipped container lines must not outweigh the single real indented block",
2664 );
2665 }
2666
2667 #[test]
2668 fn test_consistent_style_fenced_wins_over_skipped_indented() {
2669 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2673 let content = "# Heading\n\
2674 \n\
2675 <!--\n \
2676 skipped indented region one\n \
2677 more of region one\n\
2678 -->\n\
2679 \n\
2680 <!--\n \
2681 skipped indented region two\n \
2682 more of region two\n\
2683 -->\n\
2684 \n\
2685 ```md\n\
2686 real fenced block\n\
2687 ```\n";
2688
2689 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2690 let result = rule.check(&ctx).unwrap();
2691
2692 assert_eq!(
2693 result,
2694 vec![],
2695 "Fenced block must win when all indented lines are inside skipped containers",
2696 );
2697 }
2698
2699 #[test]
2700 fn test_four_space_indented_fence_is_not_valid_fence() {
2701 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2704
2705 assert!(rule.is_fenced_code_block_start("```"));
2707 assert!(rule.is_fenced_code_block_start(" ```"));
2708 assert!(rule.is_fenced_code_block_start(" ```"));
2709 assert!(rule.is_fenced_code_block_start(" ```"));
2710
2711 assert!(!rule.is_fenced_code_block_start(" ```"));
2713 assert!(!rule.is_fenced_code_block_start(" ```"));
2714 assert!(!rule.is_fenced_code_block_start(" ```"));
2715
2716 assert!(!rule.is_fenced_code_block_start("\t```"));
2718 }
2719
2720 #[test]
2721 fn test_issue_237_indented_fenced_block_detected_as_indented() {
2722 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2728
2729 let content = r#"## Test
2731
2732 ```js
2733 var foo = "hello";
2734 ```
2735"#;
2736
2737 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2738 let result = rule.check(&ctx).unwrap();
2739
2740 assert_eq!(
2742 result.len(),
2743 1,
2744 "4-space indented fence should be detected as indented code block"
2745 );
2746 assert!(
2747 result[0].message.contains("Use fenced code blocks"),
2748 "Expected 'Use fenced code blocks' message"
2749 );
2750 }
2751
2752 #[test]
2753 fn test_issue_276_indented_code_in_list() {
2754 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2757
2758 let content = r#"1. First item
27592. Second item with code:
2760
2761 # This is a code block in a list
2762 print("Hello, world!")
2763
27644. Third item"#;
2765
2766 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2767 let result = rule.check(&ctx).unwrap();
2768
2769 assert!(
2771 !result.is_empty(),
2772 "Indented code block inside list should be flagged when style=fenced"
2773 );
2774 assert!(
2775 result[0].message.contains("Use fenced code blocks"),
2776 "Expected 'Use fenced code blocks' message"
2777 );
2778 }
2779
2780 #[test]
2781 fn test_three_space_indented_fence_is_valid() {
2782 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2784
2785 let content = r#"## Test
2786
2787 ```js
2788 var foo = "hello";
2789 ```
2790"#;
2791
2792 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2793 let result = rule.check(&ctx).unwrap();
2794
2795 assert_eq!(
2797 result.len(),
2798 0,
2799 "3-space indented fence should be recognized as valid fenced code block"
2800 );
2801 }
2802
2803 #[test]
2804 fn test_indented_style_with_deeply_indented_fenced() {
2805 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2808
2809 let content = r#"Text
2810
2811 ```js
2812 var foo = "hello";
2813 ```
2814
2815More text
2816"#;
2817
2818 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2819 let result = rule.check(&ctx).unwrap();
2820
2821 assert_eq!(
2824 result.len(),
2825 0,
2826 "4-space indented content should be valid when style=indented"
2827 );
2828 }
2829
2830 #[test]
2831 fn test_fix_misplaced_fenced_block() {
2832 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2835
2836 let content = r#"## Test
2837
2838 ```js
2839 var foo = "hello";
2840 ```
2841"#;
2842
2843 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2844 let fixed = rule.fix(&ctx).unwrap();
2845
2846 let expected = r#"## Test
2848
2849```js
2850var foo = "hello";
2851```
2852"#;
2853
2854 assert_eq!(fixed, expected, "Fix should remove indentation, not add more fences");
2855 }
2856
2857 #[test]
2858 fn test_fix_regular_indented_block() {
2859 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2862
2863 let content = r#"Text
2864
2865 var foo = "hello";
2866 console.log(foo);
2867
2868More text
2869"#;
2870
2871 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2872 let fixed = rule.fix(&ctx).unwrap();
2873
2874 assert!(fixed.contains("```\nvar foo"), "Should add opening fence");
2876 assert!(fixed.contains("console.log(foo);\n```"), "Should add closing fence");
2877 }
2878
2879 #[test]
2880 fn test_fix_indented_block_with_fence_like_content() {
2881 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2885
2886 let content = r#"Text
2887
2888 some code
2889 ```not a fence opener
2890 more code
2891"#;
2892
2893 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2894 let fixed = rule.fix(&ctx).unwrap();
2895
2896 assert!(fixed.contains(" some code"), "Unsafe block should be left unchanged");
2898 assert!(!fixed.contains("```\nsome code"), "Should NOT wrap unsafe block");
2899 }
2900
2901 #[test]
2902 fn test_fix_mixed_indented_and_misplaced_blocks() {
2903 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2905
2906 let content = r#"Text
2907
2908 regular indented code
2909
2910More text
2911
2912 ```python
2913 print("hello")
2914 ```
2915"#;
2916
2917 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2918 let fixed = rule.fix(&ctx).unwrap();
2919
2920 assert!(
2922 fixed.contains("```\nregular indented code\n```"),
2923 "First block should be wrapped in fences"
2924 );
2925
2926 assert!(
2928 fixed.contains("\n```python\nprint(\"hello\")\n```"),
2929 "Second block should be dedented, not double-wrapped"
2930 );
2931 assert!(
2933 !fixed.contains("```\n```python"),
2934 "Should not have nested fence openers"
2935 );
2936 }
2937
2938 #[test]
2939 fn test_md046_front_matter() {
2940 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2941 let content = "---\nmetadata:\n\n description: Indented\n---\n";
2942 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2943 let result = rule.check(&ctx).unwrap();
2944 assert_eq!(result.len(), 0);
2945 }
2946
2947 #[test]
2948 fn test_md046_fix_front_matter() {
2949 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2950 let content = "---\nmetadata:\n\n description: Indented\n---\n";
2951 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2952 let fixed = rule.fix(&ctx).unwrap();
2953 assert_eq!(fixed, content);
2954 }
2955}