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(
357 &self,
358 lines: &[&str],
359 i: usize,
360 is_mkdocs: bool,
361 ctx: &IndentContext,
362 prev_is_code: bool,
363 ) -> bool {
364 if i >= lines.len() {
365 return false;
366 }
367
368 let line = lines[i];
369
370 if line.trim().is_empty() {
375 return false;
376 }
377
378 let indent = calculate_indentation_width_default(line);
380 if indent < 4 {
381 return false;
382 }
383
384 if ctx.in_list_context[i] {
390 let crosses_baseline = ctx
391 .list_item_baseline
392 .get(i)
393 .copied()
394 .flatten()
395 .is_some_and(|base| indent >= base + 4);
396 if !crosses_baseline {
397 return false;
398 }
399 }
400
401 if is_mkdocs && ctx.in_tab_context[i] {
403 return false;
404 }
405
406 if is_mkdocs && ctx.in_admonition_context[i] {
409 return false;
410 }
411
412 if ctx.in_comment_or_html.get(i).copied().unwrap_or(false) {
418 return false;
419 }
420
421 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
429 has_blank_line_before || prev_is_code
430 }
431
432 fn indented_block_lines(&self, lines: &[&str], is_mkdocs: bool, ictx: &IndentContext<'_>) -> Vec<bool> {
443 let mut member = vec![false; lines.len()];
444 for i in 0..lines.len() {
445 let prev_is_code = i > 0 && member[i - 1];
446 member[i] = self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx, prev_is_code);
447 }
448
449 let mut i = 0;
450 while i < lines.len() {
451 if !member[i] {
452 i += 1;
453 continue;
454 }
455 let mut next = i + 1;
456 while next < lines.len() && lines[next].trim().is_empty() {
457 next += 1;
458 }
459 if next < lines.len() && member[next] {
460 member[i + 1..next].fill(true);
461 }
462 i = next;
463 }
464
465 member
466 }
467
468 fn precompute_comment_or_html_context(ctx: &crate::lint_context::LintContext, line_count: usize) -> Vec<bool> {
477 (0..line_count)
478 .map(|i| {
479 ctx.line_info(i + 1).is_some_and(|info| {
480 info.in_html_comment
481 || info.in_mdx_comment
482 || info.in_html_block
483 || info.in_jsx_block
484 || info.in_mkdocstrings
485 || info.in_footnote_definition
486 || info.blockquote.is_some()
487 || info.in_front_matter
488 })
489 })
490 .collect()
491 }
492
493 fn precompute_mkdocs_tab_context(&self, lines: &[&str]) -> Vec<bool> {
495 let mut in_tab_context = vec![false; lines.len()];
496 let mut current_tab_indent: Option<usize> = None;
497
498 for (i, line) in lines.iter().enumerate() {
499 if mkdocs_tabs::is_tab_marker(line) {
501 let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
502 current_tab_indent = Some(tab_indent);
503 in_tab_context[i] = true;
504 continue;
505 }
506
507 if let Some(tab_indent) = current_tab_indent {
509 if mkdocs_tabs::is_tab_content(line, tab_indent) {
510 in_tab_context[i] = true;
511 } else if !line.trim().is_empty() && calculate_indentation_width_default(line) < 4 {
512 current_tab_indent = None;
514 } else {
515 in_tab_context[i] = true;
517 }
518 }
519 }
520
521 in_tab_context
522 }
523
524 fn precompute_mkdocs_admonition_context(&self, lines: &[&str]) -> Vec<bool> {
533 let mut in_admonition_context = vec![false; lines.len()];
534 let mut admonition_stack: Vec<usize> = Vec::new();
536
537 for (i, line) in lines.iter().enumerate() {
538 let line_indent = calculate_indentation_width_default(line);
539
540 if mkdocs_admonitions::is_admonition_start(line) {
542 let adm_indent = mkdocs_admonitions::get_admonition_indent(line).unwrap_or(0);
543
544 while let Some(&top_indent) = admonition_stack.last() {
546 if adm_indent <= top_indent {
548 admonition_stack.pop();
549 } else {
550 break;
551 }
552 }
553
554 admonition_stack.push(adm_indent);
556 in_admonition_context[i] = true;
557 continue;
558 }
559
560 if line.trim().is_empty() {
562 if !admonition_stack.is_empty() {
563 in_admonition_context[i] = true;
564 }
565 continue;
566 }
567
568 while let Some(&top_indent) = admonition_stack.last() {
571 if line_indent >= top_indent + 4 {
573 break;
575 } else {
576 admonition_stack.pop();
578 }
579 }
580
581 if !admonition_stack.is_empty() {
583 in_admonition_context[i] = true;
584 }
585 }
586
587 in_admonition_context
588 }
589
590 fn build_indent_context(
602 &self,
603 ctx: &crate::lint_context::LintContext,
604 lines: &[&str],
605 is_mkdocs: bool,
606 ) -> OwnedIndentContext {
607 OwnedIndentContext {
608 in_list_context: self.precompute_block_continuation_context(lines),
609 in_tab_context: if is_mkdocs {
610 self.precompute_mkdocs_tab_context(lines)
611 } else {
612 vec![false; lines.len()]
613 },
614 in_admonition_context: if is_mkdocs {
615 self.precompute_mkdocs_admonition_context(lines)
616 } else {
617 vec![false; lines.len()]
618 },
619 in_comment_or_html: Self::precompute_comment_or_html_context(ctx, lines.len()),
620 list_item_baseline: self.precompute_list_item_baseline(ctx, lines),
621 }
622 }
623
624 fn categorize_indented_blocks(&self, lines: &[&str], block_lines: &[bool]) -> (Vec<bool>, Vec<bool>) {
636 let mut is_misplaced = vec![false; lines.len()];
637 let mut contains_fences = vec![false; lines.len()];
638
639 let mut i = 0;
641 while i < lines.len() {
642 if !block_lines[i] {
644 i += 1;
645 continue;
646 }
647
648 let block_start = i;
650 let mut block_end = i;
651
652 while block_end < lines.len() && block_lines[block_end] {
653 block_end += 1;
654 }
655
656 if block_end > block_start {
658 let first_line = lines[block_start].trim_start();
659 let last_line = lines[block_end - 1].trim_start();
660
661 let is_backtick_fence = first_line.starts_with("```");
663 let is_tilde_fence = first_line.starts_with("~~~");
664
665 if is_backtick_fence || is_tilde_fence {
666 let fence_char = if is_backtick_fence { '`' } else { '~' };
667 let opener_len = first_line.chars().take_while(|&c| c == fence_char).count();
668
669 let closer_fence_len = last_line.chars().take_while(|&c| c == fence_char).count();
671 let after_closer = &last_line[closer_fence_len..];
672
673 if closer_fence_len >= opener_len && after_closer.trim().is_empty() {
674 is_misplaced[block_start..block_end].fill(true);
676 } else {
677 contains_fences[block_start..block_end].fill(true);
679 }
680 } else {
681 let has_fence_markers = (block_start..block_end).any(|j| {
684 let trimmed = lines[j].trim_start();
685 trimmed.starts_with("```") || trimmed.starts_with("~~~")
686 });
687
688 if has_fence_markers {
689 contains_fences[block_start..block_end].fill(true);
690 }
691 }
692 }
693
694 i = block_end;
695 }
696
697 (is_misplaced, contains_fences)
698 }
699
700 fn check_unclosed_code_blocks(&self, ctx: &crate::lint_context::LintContext) -> Vec<LintWarning> {
701 let mut warnings = Vec::new();
702 let lines = ctx.raw_lines();
703
704 let has_markdown_doc_block = ctx.code_block_details.iter().any(|d| {
706 if !d.is_fenced {
707 return false;
708 }
709 let lang = d.info_string.to_lowercase();
710 lang.starts_with("markdown") || lang.starts_with("md")
711 });
712
713 if has_markdown_doc_block {
716 return warnings;
717 }
718
719 for detail in &ctx.code_block_details {
720 if !detail.is_fenced {
721 continue;
722 }
723
724 if detail.end != ctx.content.len() {
726 continue;
727 }
728
729 let opening_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
731 Ok(idx) => idx,
732 Err(idx) => idx.saturating_sub(1),
733 };
734
735 let line = lines.get(opening_line_idx).unwrap_or(&"");
737 let trimmed = line.trim();
738 let fence_marker = if let Some(pos) = trimmed.find("```") {
739 let count = trimmed[pos..].chars().take_while(|&c| c == '`').count();
740 "`".repeat(count)
741 } else if let Some(pos) = trimmed.find("~~~") {
742 let count = trimmed[pos..].chars().take_while(|&c| c == '~').count();
743 "~".repeat(count)
744 } else {
745 "```".to_string()
746 };
747
748 let last_non_empty_line = lines.iter().rev().find(|l| !l.trim().is_empty()).unwrap_or(&"");
750 let last_trimmed = last_non_empty_line.trim();
751 let fence_char = fence_marker.chars().next().unwrap_or('`');
752
753 let has_closing_fence = if fence_char == '`' {
754 last_trimmed.starts_with("```") && {
755 let fence_len = last_trimmed.chars().take_while(|&c| c == '`').count();
756 last_trimmed[fence_len..].trim().is_empty()
757 }
758 } else {
759 last_trimmed.starts_with("~~~") && {
760 let fence_len = last_trimmed.chars().take_while(|&c| c == '~').count();
761 last_trimmed[fence_len..].trim().is_empty()
762 }
763 };
764
765 if !has_closing_fence {
766 if ctx
768 .lines
769 .get(opening_line_idx)
770 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
771 {
772 continue;
773 }
774
775 let (start_line, start_col, end_line, end_col) = calculate_line_range(opening_line_idx + 1, line);
776
777 warnings.push(LintWarning {
778 rule_name: Some(self.name().to_string()),
779 line: start_line,
780 column: start_col,
781 end_line,
782 end_column: end_col,
783 message: format!("Code block opened with '{fence_marker}' but never closed"),
784 severity: Severity::Warning,
785 fix: Some(Fix::new(
786 ctx.content.len()..ctx.content.len(),
787 format!("\n{fence_marker}"),
788 )),
789 });
790 }
791 }
792
793 warnings
794 }
795
796 fn detect_style(
797 &self,
798 ctx: &crate::lint_context::LintContext,
799 lines: &[&str],
800 is_mkdocs: bool,
801 ictx: &IndentContext,
802 ) -> Option<CodeBlockStyle> {
803 if lines.is_empty() {
804 return None;
805 }
806
807 let block_lines = self.indented_block_lines(lines, is_mkdocs, ictx);
808
809 let mut fenced_count = 0;
810 let mut indented_count = 0;
811
812 let mut in_fenced = false;
822 let mut prev_was_indented = false;
823
824 for (i, line) in lines.iter().enumerate() {
825 let in_container = ictx.in_comment_or_html.get(i).copied().unwrap_or(false);
826
827 if ctx.flavor.supports_colon_code_fences() && ctx.lines.get(i).is_some_and(|l| l.in_code_block) {
831 prev_was_indented = false;
832 continue;
833 }
834
835 if ctx.flavor.supports_myst_directives() && ctx.lines.get(i).is_some_and(|l| l.in_myst_directive) {
837 prev_was_indented = false;
838 continue;
839 }
840
841 if self.is_fenced_code_block_start(line) {
842 if in_container {
843 prev_was_indented = false;
846 continue;
847 }
848 if !in_fenced {
849 fenced_count += 1;
851 in_fenced = true;
852 } else {
853 in_fenced = false;
855 }
856 prev_was_indented = false;
857 } else if !in_fenced && block_lines[i] {
858 if !prev_was_indented {
860 indented_count += 1;
861 }
862 prev_was_indented = true;
863 } else {
864 prev_was_indented = false;
865 }
866 }
867
868 if fenced_count == 0 && indented_count == 0 {
869 None
870 } else if fenced_count > 0 && indented_count == 0 {
871 Some(CodeBlockStyle::Fenced)
872 } else if fenced_count == 0 && indented_count > 0 {
873 Some(CodeBlockStyle::Indented)
874 } else if fenced_count >= indented_count {
875 Some(CodeBlockStyle::Fenced)
876 } else {
877 Some(CodeBlockStyle::Indented)
878 }
879 }
880}
881
882impl Rule for MD046CodeBlockStyle {
883 fn name(&self) -> &'static str {
884 "MD046"
885 }
886
887 fn description(&self) -> &'static str {
888 "Code blocks should use a consistent style"
889 }
890
891 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
892 if ctx.content.is_empty() {
894 return Ok(Vec::new());
895 }
896
897 if !ctx.content.contains("```")
899 && !ctx.content.contains("~~~")
900 && !ctx.content.contains(" ")
901 && !ctx.content.contains('\t')
902 {
903 return Ok(Vec::new());
904 }
905
906 let unclosed_warnings = self.check_unclosed_code_blocks(ctx);
908
909 if !unclosed_warnings.is_empty() {
911 return Ok(unclosed_warnings);
912 }
913
914 let lines = ctx.raw_lines();
916 let mut warnings = Vec::new();
917
918 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
919
920 let target_style = match self.config.style {
922 CodeBlockStyle::Consistent => {
923 let owned = self.build_indent_context(ctx, lines, is_mkdocs);
924 self.detect_style(ctx, lines, is_mkdocs, &owned.borrow())
925 .unwrap_or(CodeBlockStyle::Fenced)
926 }
927 _ => self.config.style,
928 };
929
930 let mut reported_indented_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
932
933 for detail in &ctx.code_block_details {
934 if detail.start >= ctx.content.len() || detail.end > ctx.content.len() {
935 continue;
936 }
937
938 let start_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
939 Ok(idx) => idx,
940 Err(idx) => idx.saturating_sub(1),
941 };
942
943 if detail.is_fenced {
944 if target_style == CodeBlockStyle::Indented {
945 let line = lines.get(start_line_idx).unwrap_or(&"");
946
947 if ctx
948 .lines
949 .get(start_line_idx)
950 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment || info.in_footnote_definition)
951 {
952 continue;
953 }
954
955 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
956 warnings.push(LintWarning {
957 rule_name: Some(self.name().to_string()),
958 line: start_line,
959 column: start_col,
960 end_line,
961 end_column: end_col,
962 message: "Use indented code blocks".to_string(),
963 severity: Severity::Warning,
964 fix: None,
965 });
966 }
967 } else {
968 if target_style == CodeBlockStyle::Fenced && !reported_indented_lines.contains(&start_line_idx) {
970 let line = lines.get(start_line_idx).unwrap_or(&"");
971
972 if ctx.lines.get(start_line_idx).is_some_and(|info| {
974 info.in_html_comment
975 || info.in_mdx_comment
976 || info.in_html_block
977 || info.in_jsx_block
978 || info.in_mkdocstrings
979 || info.in_footnote_definition
980 || info.blockquote.is_some()
981 || info.in_front_matter
982 }) {
983 continue;
984 }
985
986 if is_mkdocs
988 && ctx
989 .lines
990 .get(start_line_idx)
991 .is_some_and(|info| info.in_admonition || info.in_content_tab)
992 {
993 continue;
994 }
995
996 reported_indented_lines.insert(start_line_idx);
997
998 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
999 warnings.push(LintWarning {
1000 rule_name: Some(self.name().to_string()),
1001 line: start_line,
1002 column: start_col,
1003 end_line,
1004 end_column: end_col,
1005 message: "Use fenced code blocks".to_string(),
1006 severity: Severity::Warning,
1007 fix: None,
1008 });
1009 }
1010 }
1011 }
1012
1013 warnings.sort_by_key(|w| (w.line, w.column));
1015
1016 Ok(warnings)
1017 }
1018
1019 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
1020 let content = ctx.content;
1021 if content.is_empty() {
1022 return Ok(String::new());
1023 }
1024
1025 let lines = ctx.raw_lines();
1026
1027 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
1029
1030 let owned = self.build_indent_context(ctx, lines, is_mkdocs);
1031 let ictx = owned.borrow();
1032
1033 let target_style = match self.config.style {
1034 CodeBlockStyle::Consistent => self
1035 .detect_style(ctx, lines, is_mkdocs, &ictx)
1036 .unwrap_or(CodeBlockStyle::Fenced),
1037 _ => self.config.style,
1038 };
1039
1040 let block_lines = self.indented_block_lines(lines, is_mkdocs, &ictx);
1041
1042 let (misplaced_fence_lines, unsafe_fence_lines) = self.categorize_indented_blocks(lines, &block_lines);
1046
1047 let mut result = String::with_capacity(content.len());
1048 let mut in_fenced_block = false;
1049 let mut fenced_fence_opener: Option<(char, usize)> = None;
1053 let mut in_indented_block = false;
1054 let mut current_block_fence_indent = String::new();
1059
1060 let mut current_block_disabled = false;
1062
1063 for (i, line) in lines.iter().enumerate() {
1064 let line_num = i + 1;
1065 let trimmed = line.trim_start();
1066
1067 if !in_fenced_block
1070 && Self::has_valid_fence_indent(line)
1071 && (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
1072 {
1073 current_block_disabled = ctx.inline_config().is_rule_disabled(self.name(), line_num);
1075 in_fenced_block = true;
1076 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
1077 let opener_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1078 fenced_fence_opener = Some((fence_char, opener_len));
1079
1080 if current_block_disabled {
1081 result.push_str(line);
1083 result.push('\n');
1084 } else if target_style == CodeBlockStyle::Indented {
1085 in_indented_block = true;
1087 } else {
1088 result.push_str(line);
1090 result.push('\n');
1091 }
1092 } else if in_fenced_block && fenced_fence_opener.is_some() {
1093 let (fence_char, opener_len) = fenced_fence_opener.unwrap();
1094 let closer_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1097 let after_closer = &trimmed[closer_len..];
1098 let is_closer = closer_len >= opener_len && after_closer.trim().is_empty() && closer_len > 0;
1099 if is_closer {
1100 in_fenced_block = false;
1101 fenced_fence_opener = None;
1102 in_indented_block = false;
1103
1104 if current_block_disabled {
1105 result.push_str(line);
1106 result.push('\n');
1107 } else if target_style == CodeBlockStyle::Indented {
1108 } else {
1110 result.push_str(line);
1112 result.push('\n');
1113 }
1114 current_block_disabled = false;
1115 } else if current_block_disabled {
1116 result.push_str(line);
1118 result.push('\n');
1119 } else if target_style == CodeBlockStyle::Indented {
1120 if !line.is_empty() {
1127 result.push_str(" ");
1128 result.push_str(line);
1129 }
1130 result.push('\n');
1131 } else {
1132 result.push_str(line);
1134 result.push('\n');
1135 }
1136 } else if block_lines[i] {
1137 if ctx.inline_config().is_rule_disabled(self.name(), line_num) {
1141 result.push_str(line);
1142 result.push('\n');
1143 continue;
1144 }
1145
1146 let prev_line_is_indented = i > 0 && block_lines[i - 1];
1148
1149 if target_style == CodeBlockStyle::Fenced {
1150 let baseline = ictx.list_item_baseline.get(i).copied().flatten().unwrap_or(0);
1156 let body = if line.trim().is_empty() {
1164 ""
1165 } else {
1166 line.strip_prefix(" ").unwrap_or(line)
1167 };
1168
1169 if misplaced_fence_lines[i] {
1172 result.push_str(line.trim_start());
1174 result.push('\n');
1175 } else if unsafe_fence_lines[i] {
1176 result.push_str(line);
1179 result.push('\n');
1180 } else if !prev_line_is_indented && !in_indented_block {
1181 current_block_fence_indent = " ".repeat(baseline);
1183 result.push_str(¤t_block_fence_indent);
1184 result.push_str("```\n");
1185 result.push_str(body);
1186 result.push('\n');
1187 in_indented_block = true;
1188 } else {
1189 result.push_str(body);
1191 result.push('\n');
1192 }
1193
1194 let next_line_is_indented = i < lines.len() - 1 && block_lines[i + 1];
1196 if !next_line_is_indented
1198 && in_indented_block
1199 && !misplaced_fence_lines[i]
1200 && !unsafe_fence_lines[i]
1201 {
1202 result.push_str(¤t_block_fence_indent);
1203 result.push_str("```\n");
1204 in_indented_block = false;
1205 current_block_fence_indent.clear();
1206 }
1207 } else {
1208 result.push_str(line);
1210 result.push('\n');
1211 }
1212 } else {
1213 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1215 result.push_str(¤t_block_fence_indent);
1216 result.push_str("```\n");
1217 in_indented_block = false;
1218 current_block_fence_indent.clear();
1219 }
1220
1221 result.push_str(line);
1222 result.push('\n');
1223 }
1224 }
1225
1226 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1228 result.push_str(¤t_block_fence_indent);
1229 result.push_str("```\n");
1230 }
1231
1232 if let Some((fence_char, opener_len)) = fenced_fence_opener
1238 && in_fenced_block
1239 {
1240 let has_unclosed_violation = !self.check_unclosed_code_blocks(ctx).is_empty();
1241 if has_unclosed_violation {
1242 let closer: String = std::iter::repeat_n(fence_char, opener_len).collect();
1243 result.push_str(&closer);
1244 result.push('\n');
1245 }
1246 }
1247
1248 if !content.ends_with('\n') && result.ends_with('\n') {
1250 result.pop();
1251 }
1252
1253 Ok(result)
1254 }
1255
1256 fn category(&self) -> RuleCategory {
1258 RuleCategory::CodeBlock
1259 }
1260
1261 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
1263 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
1266 }
1267
1268 fn as_any(&self) -> &dyn std::any::Any {
1269 self
1270 }
1271
1272 crate::impl_rule_config_methods!(MD046Config);
1273}
1274
1275#[cfg(test)]
1276mod tests {
1277 use super::*;
1278 use crate::lint_context::LintContext;
1279
1280 fn detect_style_from_content(rule: &MD046CodeBlockStyle, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
1292 let flavor = if is_mkdocs {
1293 crate::config::MarkdownFlavor::MkDocs
1294 } else {
1295 crate::config::MarkdownFlavor::Standard
1296 };
1297 let ctx = LintContext::new(content, flavor, None);
1298 let lines: Vec<&str> = content.lines().collect();
1299 let in_list_context = rule.precompute_block_continuation_context(&lines);
1300 let in_tab_context = if is_mkdocs {
1301 rule.precompute_mkdocs_tab_context(&lines)
1302 } else {
1303 vec![false; lines.len()]
1304 };
1305 let in_admonition_context = if is_mkdocs {
1306 rule.precompute_mkdocs_admonition_context(&lines)
1307 } else {
1308 vec![false; lines.len()]
1309 };
1310 let in_comment_or_html = vec![false; lines.len()];
1311 let list_item_baseline: Vec<Option<usize>> = vec![None; lines.len()];
1317 let ictx = IndentContext {
1318 in_list_context: &in_list_context,
1319 in_tab_context: &in_tab_context,
1320 in_admonition_context: &in_admonition_context,
1321 in_comment_or_html: &in_comment_or_html,
1322 list_item_baseline: &list_item_baseline,
1323 };
1324 rule.detect_style(&ctx, &lines, is_mkdocs, &ictx)
1325 }
1326
1327 #[test]
1328 fn test_fenced_code_block_detection() {
1329 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1330 assert!(rule.is_fenced_code_block_start("```"));
1331 assert!(rule.is_fenced_code_block_start("```rust"));
1332 assert!(rule.is_fenced_code_block_start("~~~"));
1333 assert!(rule.is_fenced_code_block_start("~~~python"));
1334 assert!(rule.is_fenced_code_block_start(" ```"));
1335 assert!(!rule.is_fenced_code_block_start("``"));
1336 assert!(!rule.is_fenced_code_block_start("~~"));
1337 assert!(!rule.is_fenced_code_block_start("Regular text"));
1338 }
1339
1340 #[test]
1341 fn test_consistent_style_with_fenced_blocks() {
1342 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1343 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
1344 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1345 let result = rule.check(&ctx).unwrap();
1346
1347 assert_eq!(result.len(), 0);
1349 }
1350
1351 #[test]
1352 fn test_consistent_style_with_indented_blocks() {
1353 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1354 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
1355 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1356 let result = rule.check(&ctx).unwrap();
1357
1358 assert_eq!(result.len(), 0);
1360 }
1361
1362 #[test]
1363 fn test_consistent_style_mixed() {
1364 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1365 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
1366 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1367 let result = rule.check(&ctx).unwrap();
1368
1369 assert!(!result.is_empty());
1371 }
1372
1373 #[test]
1374 fn test_fenced_style_with_indented_blocks() {
1375 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1376 let content = "Text\n\n indented code\n more code\n\nMore text";
1377 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1378 let result = rule.check(&ctx).unwrap();
1379
1380 assert!(!result.is_empty());
1382 assert!(result[0].message.contains("Use fenced code blocks"));
1383 }
1384
1385 #[test]
1386 fn test_fenced_style_with_tab_indented_blocks() {
1387 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1388 let content = "Text\n\n\ttab indented code\n\tmore code\n\nMore text";
1389 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1390 let result = rule.check(&ctx).unwrap();
1391
1392 assert!(!result.is_empty());
1394 assert!(result[0].message.contains("Use fenced code blocks"));
1395 }
1396
1397 #[test]
1398 fn test_fenced_style_with_mixed_whitespace_indented_blocks() {
1399 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1400 let content = "Text\n\n \tmixed indent code\n \tmore code\n\nMore text";
1402 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1403 let result = rule.check(&ctx).unwrap();
1404
1405 assert!(
1407 !result.is_empty(),
1408 "Mixed whitespace (2 spaces + tab) should be detected as indented code"
1409 );
1410 assert!(result[0].message.contains("Use fenced code blocks"));
1411 }
1412
1413 #[test]
1414 fn test_fenced_style_with_one_space_tab_indent() {
1415 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1416 let content = "Text\n\n \ttab after one space\n \tmore code\n\nMore text";
1418 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1419 let result = rule.check(&ctx).unwrap();
1420
1421 assert!(!result.is_empty(), "1 space + tab should be detected as indented code");
1422 assert!(result[0].message.contains("Use fenced code blocks"));
1423 }
1424
1425 #[test]
1426 fn test_indented_style_with_fenced_blocks() {
1427 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1428 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
1429 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1430 let result = rule.check(&ctx).unwrap();
1431
1432 assert!(!result.is_empty());
1434 assert!(result[0].message.contains("Use indented code blocks"));
1435 }
1436
1437 #[test]
1438 fn test_unclosed_code_block() {
1439 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1440 let content = "```\ncode without closing fence";
1441 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1442 let result = rule.check(&ctx).unwrap();
1443
1444 assert_eq!(result.len(), 1);
1445 assert!(result[0].message.contains("never closed"));
1446 }
1447
1448 #[test]
1449 fn test_nested_code_blocks() {
1450 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1451 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
1452 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1453 let result = rule.check(&ctx).unwrap();
1454
1455 assert_eq!(result.len(), 0);
1457 }
1458
1459 #[test]
1460 fn test_fix_indented_to_fenced() {
1461 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1462 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
1463 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1464 let fixed = rule.fix(&ctx).unwrap();
1465
1466 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
1467 }
1468
1469 #[test]
1470 fn test_fix_fenced_to_indented() {
1471 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1472 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
1473 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1474 let fixed = rule.fix(&ctx).unwrap();
1475
1476 assert!(fixed.contains(" code line 1\n code line 2"));
1477 assert!(!fixed.contains("```"));
1478 }
1479
1480 #[test]
1481 fn test_fix_fenced_to_indented_blank_lines_have_no_trailing_spaces() {
1482 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1486 let content = "Text\n\n```\ncode line 1\n\ncode line 2\n```\n\nMore text";
1487 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1488 let fixed = rule.fix(&ctx).unwrap();
1489
1490 for line in fixed.lines() {
1491 assert!(
1492 line.is_empty() || !line.trim_end().is_empty() || line == line.trim_end(),
1493 "no line may have trailing whitespace, got {line:?}"
1494 );
1495 assert_ne!(line, " ", "blank line was indented to trailing spaces");
1496 }
1497 assert!(fixed.contains(" code line 1\n\n code line 2"));
1499 }
1500
1501 #[test]
1502 fn test_is_list_item_requires_delimiter_after_digits() {
1503 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1504 assert!(rule.is_list_item("1. First"));
1506 assert!(rule.is_list_item("42) Item"));
1507 assert!(rule.is_list_item(" 3. Indented item"));
1508 assert!(rule.is_list_item("- bullet"));
1510 assert!(rule.is_list_item("* bullet"));
1511 assert!(!rule.is_list_item("2 results. More info."));
1514 assert!(!rule.is_list_item("3 options (a, b) here"));
1515 assert!(!rule.is_list_item("100 items in stock. Buy now"));
1516 }
1517
1518 #[test]
1519 fn test_fix_fenced_to_indented_preserves_internal_indentation() {
1520 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1523 let content = r#"# Test
1524
1525```html
1526<!doctype html>
1527<html>
1528 <head>
1529 <title>Test</title>
1530 </head>
1531</html>
1532```
1533"#;
1534 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1535 let fixed = rule.fix(&ctx).unwrap();
1536
1537 assert!(
1540 fixed.contains(" <head>"),
1541 "Expected 6 spaces before <head> (4 for code block + 2 original), got:\n{fixed}"
1542 );
1543 assert!(
1544 fixed.contains(" <title>"),
1545 "Expected 8 spaces before <title> (4 for code block + 4 original), got:\n{fixed}"
1546 );
1547 assert!(!fixed.contains("```"), "Fenced markers should be removed");
1548 }
1549
1550 #[test]
1551 fn test_fix_fenced_to_indented_preserves_python_indentation() {
1552 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1554 let content = r#"# Python Example
1555
1556```python
1557def greet(name):
1558 if name:
1559 print(f"Hello, {name}!")
1560 else:
1561 print("Hello, World!")
1562```
1563"#;
1564 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1565 let fixed = rule.fix(&ctx).unwrap();
1566
1567 assert!(
1569 fixed.contains(" def greet(name):"),
1570 "Function def should have 4 spaces (code block indent)"
1571 );
1572 assert!(
1573 fixed.contains(" if name:"),
1574 "if statement should have 8 spaces (4 code + 4 Python)"
1575 );
1576 assert!(
1577 fixed.contains(" print"),
1578 "print should have 12 spaces (4 code + 8 Python)"
1579 );
1580 }
1581
1582 #[test]
1583 fn test_fix_fenced_to_indented_preserves_yaml_indentation() {
1584 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1586 let content = r#"# Config
1587
1588```yaml
1589server:
1590 host: localhost
1591 port: 8080
1592 ssl:
1593 enabled: true
1594 cert: /path/to/cert
1595```
1596"#;
1597 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1598 let fixed = rule.fix(&ctx).unwrap();
1599
1600 assert!(fixed.contains(" server:"), "Root key should have 4 spaces");
1601 assert!(fixed.contains(" host:"), "First level should have 6 spaces");
1602 assert!(fixed.contains(" ssl:"), "ssl key should have 6 spaces");
1603 assert!(fixed.contains(" enabled:"), "Nested ssl should have 8 spaces");
1604 }
1605
1606 #[test]
1607 fn test_fix_fenced_to_indented_preserves_empty_lines() {
1608 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1612 let content = "```\nline1\n\nline2\n```\n";
1613 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1614 let fixed = rule.fix(&ctx).unwrap();
1615
1616 assert!(fixed.contains(" line1"), "line1 should be indented");
1618 assert!(fixed.contains(" line2"), "line2 should be indented");
1619 assert!(
1620 fixed.contains(" line1\n\n line2"),
1621 "blank line must stay empty, got {fixed:?}"
1622 );
1623 }
1624
1625 #[test]
1626 fn test_fix_fenced_to_indented_multiple_blocks() {
1627 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1629 let content = r#"# Doc
1630
1631```python
1632def foo():
1633 pass
1634```
1635
1636Text between.
1637
1638```yaml
1639key:
1640 value: 1
1641```
1642"#;
1643 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1644 let fixed = rule.fix(&ctx).unwrap();
1645
1646 assert!(fixed.contains(" def foo():"), "Python def should be indented");
1647 assert!(fixed.contains(" pass"), "Python body should have 8 spaces");
1648 assert!(fixed.contains(" key:"), "YAML root should have 4 spaces");
1649 assert!(fixed.contains(" value:"), "YAML nested should have 6 spaces");
1650 assert!(!fixed.contains("```"), "No fence markers should remain");
1651 }
1652
1653 #[test]
1654 fn test_fix_unclosed_block() {
1655 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1656 let content = "```\ncode without closing";
1657 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1658 let fixed = rule.fix(&ctx).unwrap();
1659
1660 assert!(fixed.ends_with("```"));
1662 }
1663
1664 #[test]
1665 fn test_code_block_in_list() {
1666 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1667 let content = "- List item\n code in list\n more code\n- Next item";
1668 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1669 let result = rule.check(&ctx).unwrap();
1670
1671 assert_eq!(result.len(), 0);
1673 }
1674
1675 #[test]
1676 fn test_detect_style_fenced() {
1677 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1678 let content = "```\ncode\n```";
1679 let style = detect_style_from_content(&rule, content, false);
1680
1681 assert_eq!(style, Some(CodeBlockStyle::Fenced));
1682 }
1683
1684 #[test]
1685 fn test_detect_style_indented() {
1686 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1687 let content = "Text\n\n code\n\nMore";
1688 let style = detect_style_from_content(&rule, content, false);
1689
1690 assert_eq!(style, Some(CodeBlockStyle::Indented));
1691 }
1692
1693 #[test]
1694 fn test_detect_style_none() {
1695 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1696 let content = "No code blocks here";
1697 let style = detect_style_from_content(&rule, content, false);
1698
1699 assert_eq!(style, None);
1700 }
1701
1702 #[test]
1703 fn test_tilde_fence() {
1704 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1705 let content = "~~~\ncode\n~~~";
1706 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1707 let result = rule.check(&ctx).unwrap();
1708
1709 assert_eq!(result.len(), 0);
1711 }
1712
1713 #[test]
1714 fn test_language_specification() {
1715 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1716 let content = "```rust\nfn main() {}\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);
1721 }
1722
1723 #[test]
1724 fn test_empty_content() {
1725 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1726 let content = "";
1727 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1728 let result = rule.check(&ctx).unwrap();
1729
1730 assert_eq!(result.len(), 0);
1731 }
1732
1733 #[test]
1734 fn test_default_config() {
1735 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1736 let (name, _config) = rule.default_config_section().unwrap();
1737 assert_eq!(name, "MD046");
1738 }
1739
1740 #[test]
1741 fn test_markdown_documentation_block() {
1742 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1743 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
1744 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1745 let result = rule.check(&ctx).unwrap();
1746
1747 assert_eq!(result.len(), 0);
1749 }
1750
1751 #[test]
1752 fn test_preserve_trailing_newline() {
1753 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1754 let content = "```\ncode\n```\n";
1755 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1756 let fixed = rule.fix(&ctx).unwrap();
1757
1758 assert_eq!(fixed, content);
1759 }
1760
1761 #[test]
1762 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
1763 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1764 let content = r#"# Document
1765
1766=== "Python"
1767
1768 This is tab content
1769 Not an indented code block
1770
1771 ```python
1772 def hello():
1773 print("Hello")
1774 ```
1775
1776=== "JavaScript"
1777
1778 More tab content here
1779 Also not an indented code block"#;
1780
1781 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1782 let result = rule.check(&ctx).unwrap();
1783
1784 assert_eq!(result.len(), 0);
1786 }
1787
1788 #[test]
1789 fn test_mkdocs_tabs_with_actual_indented_code() {
1790 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1791 let content = r#"# Document
1792
1793=== "Tab 1"
1794
1795 This is tab content
1796
1797Regular text
1798
1799 This is an actual indented code block
1800 Should be flagged"#;
1801
1802 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1803 let result = rule.check(&ctx).unwrap();
1804
1805 assert_eq!(result.len(), 1);
1807 assert!(result[0].message.contains("Use fenced code blocks"));
1808 }
1809
1810 #[test]
1811 fn test_mkdocs_tabs_detect_style() {
1812 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1813 let content = r#"=== "Tab 1"
1814
1815 Content in tab
1816 More content
1817
1818=== "Tab 2"
1819
1820 Content in second tab"#;
1821
1822 let style = detect_style_from_content(&rule, content, true);
1824 assert_eq!(style, None); let style = detect_style_from_content(&rule, content, false);
1828 assert_eq!(style, Some(CodeBlockStyle::Indented));
1829 }
1830
1831 #[test]
1832 fn test_mkdocs_nested_tabs() {
1833 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1834 let content = r#"# Document
1835
1836=== "Outer Tab"
1837
1838 Some content
1839
1840 === "Nested Tab"
1841
1842 Nested tab content
1843 Should not be flagged"#;
1844
1845 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1846 let result = rule.check(&ctx).unwrap();
1847
1848 assert_eq!(result.len(), 0);
1850 }
1851
1852 #[test]
1853 fn test_mkdocs_admonitions_not_flagged_as_indented_code() {
1854 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1857 let content = r#"# Document
1858
1859!!! note
1860 This is normal admonition content, not a code block.
1861 It spans multiple lines.
1862
1863??? warning "Collapsible Warning"
1864 This is also admonition content.
1865
1866???+ tip "Expanded Tip"
1867 And this one too.
1868
1869Regular text outside admonitions."#;
1870
1871 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1872 let result = rule.check(&ctx).unwrap();
1873
1874 assert_eq!(
1876 result.len(),
1877 0,
1878 "Admonition content in MkDocs mode should not trigger MD046"
1879 );
1880 }
1881
1882 #[test]
1883 fn test_mkdocs_admonition_with_actual_indented_code() {
1884 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1886 let content = r#"# Document
1887
1888!!! note
1889 This is admonition content.
1890
1891Regular text ends the admonition.
1892
1893 This is actual indented code (should be flagged)"#;
1894
1895 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1896 let result = rule.check(&ctx).unwrap();
1897
1898 assert_eq!(result.len(), 1);
1900 assert!(result[0].message.contains("Use fenced code blocks"));
1901 }
1902
1903 #[test]
1904 fn test_admonition_in_standard_mode_flagged() {
1905 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1909 let content = r#"# Document
1910
1911!!! note
1912
1913 This looks like code in standard mode.
1914
1915Regular text."#;
1916
1917 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1919 let result = rule.check(&ctx).unwrap();
1920
1921 assert_eq!(
1923 result.len(),
1924 1,
1925 "Admonition content in Standard mode should be flagged as indented code"
1926 );
1927 }
1928
1929 #[test]
1930 fn test_mkdocs_admonition_with_fenced_code_inside() {
1931 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1933 let content = r#"# Document
1934
1935!!! note "Code Example"
1936 Here's some code:
1937
1938 ```python
1939 def hello():
1940 print("world")
1941 ```
1942
1943 More text after code.
1944
1945Regular text."#;
1946
1947 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1948 let result = rule.check(&ctx).unwrap();
1949
1950 assert_eq!(result.len(), 0, "Fenced code blocks inside admonitions should be valid");
1952 }
1953
1954 #[test]
1955 fn test_mkdocs_nested_admonitions() {
1956 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1958 let content = r#"# Document
1959
1960!!! note "Outer"
1961 Outer content.
1962
1963 !!! warning "Inner"
1964 Inner content.
1965 More inner content.
1966
1967 Back to outer.
1968
1969Regular text."#;
1970
1971 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1972 let result = rule.check(&ctx).unwrap();
1973
1974 assert_eq!(result.len(), 0, "Nested admonitions should not be flagged");
1976 }
1977
1978 #[test]
1979 fn test_mkdocs_admonition_fix_does_not_wrap() {
1980 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1982 let content = r#"!!! note
1983 Content that should stay as admonition content.
1984 Not be wrapped in code fences.
1985"#;
1986
1987 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1988 let fixed = rule.fix(&ctx).unwrap();
1989
1990 assert!(
1992 !fixed.contains("```\n Content"),
1993 "Admonition content should not be wrapped in fences"
1994 );
1995 assert_eq!(fixed, content, "Content should remain unchanged");
1996 }
1997
1998 #[test]
1999 fn test_mkdocs_empty_admonition() {
2000 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2002 let content = r#"!!! note
2003
2004Regular paragraph after empty admonition.
2005
2006 This IS an indented code block (after blank + non-indented line)."#;
2007
2008 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2009 let result = rule.check(&ctx).unwrap();
2010
2011 assert_eq!(result.len(), 1, "Indented code after admonition ends should be flagged");
2013 }
2014
2015 #[test]
2016 fn test_mkdocs_indented_admonition() {
2017 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2019 let content = r#"- List item
2020
2021 !!! note
2022 Indented admonition content.
2023 More content.
2024
2025- Next item"#;
2026
2027 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2028 let result = rule.check(&ctx).unwrap();
2029
2030 assert_eq!(
2032 result.len(),
2033 0,
2034 "Indented admonitions (e.g., in lists) should not be flagged"
2035 );
2036 }
2037
2038 #[test]
2039 fn test_footnote_indented_paragraphs_not_flagged() {
2040 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2041 let content = r#"# Test Document with Footnotes
2042
2043This is some text with a footnote[^1].
2044
2045Here's some code:
2046
2047```bash
2048echo "fenced code block"
2049```
2050
2051More text with another footnote[^2].
2052
2053[^1]: Really interesting footnote text.
2054
2055 Even more interesting second paragraph.
2056
2057[^2]: Another footnote.
2058
2059 With a second paragraph too.
2060
2061 And even a third paragraph!"#;
2062
2063 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2064 let result = rule.check(&ctx).unwrap();
2065
2066 assert_eq!(result.len(), 0);
2068 }
2069
2070 #[test]
2071 fn test_footnote_definition_detection() {
2072 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2073
2074 assert!(rule.is_footnote_definition("[^1]: Footnote text"));
2077 assert!(rule.is_footnote_definition("[^foo]: Footnote text"));
2078 assert!(rule.is_footnote_definition("[^long-name]: Footnote text"));
2079 assert!(rule.is_footnote_definition("[^test_123]: Mixed chars"));
2080 assert!(rule.is_footnote_definition(" [^1]: Indented footnote"));
2081 assert!(rule.is_footnote_definition("[^a]: Minimal valid footnote"));
2082 assert!(rule.is_footnote_definition("[^123]: Numeric label"));
2083 assert!(rule.is_footnote_definition("[^_]: Single underscore"));
2084 assert!(rule.is_footnote_definition("[^-]: Single hyphen"));
2085
2086 assert!(!rule.is_footnote_definition("[^]: No label"));
2088 assert!(!rule.is_footnote_definition("[^ ]: Whitespace only"));
2089 assert!(!rule.is_footnote_definition("[^ ]: Multiple spaces"));
2090 assert!(!rule.is_footnote_definition("[^\t]: Tab only"));
2091
2092 assert!(!rule.is_footnote_definition("[^]]: Extra bracket"));
2094 assert!(!rule.is_footnote_definition("Regular text [^1]:"));
2095 assert!(!rule.is_footnote_definition("[1]: Not a footnote"));
2096 assert!(!rule.is_footnote_definition("[^")); assert!(!rule.is_footnote_definition("[^1:")); assert!(!rule.is_footnote_definition("^1]: Missing opening bracket"));
2099
2100 assert!(!rule.is_footnote_definition("[^test.name]: Period"));
2102 assert!(!rule.is_footnote_definition("[^test name]: Space in label"));
2103 assert!(!rule.is_footnote_definition("[^test@name]: Special char"));
2104 assert!(!rule.is_footnote_definition("[^test/name]: Slash"));
2105 assert!(!rule.is_footnote_definition("[^test\\name]: Backslash"));
2106
2107 assert!(!rule.is_footnote_definition("[^test\r]: Carriage return"));
2110 }
2111
2112 #[test]
2113 fn test_footnote_with_blank_lines() {
2114 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2118 let content = r#"# Document
2119
2120Text with footnote[^1].
2121
2122[^1]: First paragraph.
2123
2124 Second paragraph after blank line.
2125
2126 Third paragraph after another blank line.
2127
2128Regular text at column 0 ends the footnote."#;
2129
2130 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2131 let result = rule.check(&ctx).unwrap();
2132
2133 assert_eq!(
2135 result.len(),
2136 0,
2137 "Indented content within footnotes should not trigger MD046"
2138 );
2139 }
2140
2141 #[test]
2142 fn test_footnote_multiple_consecutive_blank_lines() {
2143 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2146 let content = r#"Text[^1].
2147
2148[^1]: First paragraph.
2149
2150
2151
2152 Content after three blank lines (still part of footnote).
2153
2154Not indented, so footnote ends here."#;
2155
2156 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2157 let result = rule.check(&ctx).unwrap();
2158
2159 assert_eq!(
2161 result.len(),
2162 0,
2163 "Multiple blank lines shouldn't break footnote continuation"
2164 );
2165 }
2166
2167 #[test]
2168 fn test_footnote_terminated_by_non_indented_content() {
2169 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2172 let content = r#"[^1]: Footnote content.
2173
2174 More indented content in footnote.
2175
2176This paragraph is not indented, so footnote ends.
2177
2178 This should be flagged as indented code block."#;
2179
2180 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2181 let result = rule.check(&ctx).unwrap();
2182
2183 assert_eq!(
2185 result.len(),
2186 1,
2187 "Indented code after footnote termination should be flagged"
2188 );
2189 assert!(
2190 result[0].message.contains("Use fenced code blocks"),
2191 "Expected MD046 warning for indented code block"
2192 );
2193 assert!(result[0].line >= 7, "Warning should be on the indented code block line");
2194 }
2195
2196 #[test]
2197 fn test_footnote_terminated_by_structural_elements() {
2198 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2200 let content = r#"[^1]: Footnote content.
2201
2202 More content.
2203
2204## Heading terminates footnote
2205
2206 This indented content should be flagged.
2207
2208---
2209
2210 This should also be flagged (after horizontal rule)."#;
2211
2212 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2213 let result = rule.check(&ctx).unwrap();
2214
2215 assert_eq!(
2217 result.len(),
2218 2,
2219 "Both indented blocks after termination should be flagged"
2220 );
2221 }
2222
2223 #[test]
2224 fn test_footnote_with_code_block_inside() {
2225 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2228 let content = r#"Text[^1].
2229
2230[^1]: Footnote with code:
2231
2232 ```python
2233 def hello():
2234 print("world")
2235 ```
2236
2237 More footnote text after code."#;
2238
2239 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2240 let result = rule.check(&ctx).unwrap();
2241
2242 assert_eq!(result.len(), 0, "Fenced code blocks within footnotes should be allowed");
2244 }
2245
2246 #[test]
2247 fn test_footnote_with_8_space_indented_code() {
2248 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2251 let content = r#"Text[^1].
2252
2253[^1]: Footnote with nested code.
2254
2255 code block
2256 more code"#;
2257
2258 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2259 let result = rule.check(&ctx).unwrap();
2260
2261 assert_eq!(
2263 result.len(),
2264 0,
2265 "8-space indented code within footnotes represents nested code blocks"
2266 );
2267 }
2268
2269 #[test]
2270 fn test_multiple_footnotes() {
2271 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2274 let content = r#"Text[^1] and more[^2].
2275
2276[^1]: First footnote.
2277
2278 Continuation of first.
2279
2280[^2]: Second footnote starts here, ending the first.
2281
2282 Continuation of second."#;
2283
2284 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2285 let result = rule.check(&ctx).unwrap();
2286
2287 assert_eq!(
2289 result.len(),
2290 0,
2291 "Multiple footnotes should each maintain their continuation context"
2292 );
2293 }
2294
2295 #[test]
2296 fn test_list_item_ends_footnote_context() {
2297 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2299 let content = r#"[^1]: Footnote.
2300
2301 Content in footnote.
2302
2303- List item starts here (ends footnote context).
2304
2305 This indented content is part of the list, not the footnote."#;
2306
2307 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2308 let result = rule.check(&ctx).unwrap();
2309
2310 assert_eq!(
2312 result.len(),
2313 0,
2314 "List items should end footnote context and start their own"
2315 );
2316 }
2317
2318 #[test]
2319 fn test_footnote_vs_actual_indented_code() {
2320 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2323 let content = r#"# Heading
2324
2325Text with footnote[^1].
2326
2327[^1]: Footnote content.
2328
2329 Part of footnote (should not be flagged).
2330
2331Regular paragraph ends footnote context.
2332
2333 This is actual indented code (MUST be flagged)
2334 Should be detected as code block"#;
2335
2336 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2337 let result = rule.check(&ctx).unwrap();
2338
2339 assert_eq!(
2341 result.len(),
2342 1,
2343 "Must still detect indented code blocks outside footnotes"
2344 );
2345 assert!(
2346 result[0].message.contains("Use fenced code blocks"),
2347 "Expected MD046 warning for indented code"
2348 );
2349 assert!(
2350 result[0].line >= 11,
2351 "Warning should be on the actual indented code line"
2352 );
2353 }
2354
2355 #[test]
2356 fn test_spec_compliant_label_characters() {
2357 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2360
2361 assert!(rule.is_footnote_definition("[^test]: text"));
2363 assert!(rule.is_footnote_definition("[^TEST]: text"));
2364 assert!(rule.is_footnote_definition("[^test-name]: text"));
2365 assert!(rule.is_footnote_definition("[^test_name]: text"));
2366 assert!(rule.is_footnote_definition("[^test123]: text"));
2367 assert!(rule.is_footnote_definition("[^123]: text"));
2368 assert!(rule.is_footnote_definition("[^a1b2c3]: text"));
2369
2370 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")); }
2378
2379 #[test]
2380 fn test_code_block_inside_html_comment() {
2381 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2384 let content = r#"# Document
2385
2386Some text.
2387
2388<!--
2389Example code block in comment:
2390
2391```typescript
2392console.log("Hello");
2393```
2394
2395More comment text.
2396-->
2397
2398More content."#;
2399
2400 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2401 let result = rule.check(&ctx).unwrap();
2402
2403 assert_eq!(
2404 result.len(),
2405 0,
2406 "Code blocks inside HTML comments should not be flagged as unclosed"
2407 );
2408 }
2409
2410 #[test]
2411 fn test_unclosed_fence_inside_html_comment() {
2412 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2414 let content = r#"# Document
2415
2416<!--
2417Example with intentionally unclosed fence:
2418
2419```
2420code without closing
2421-->
2422
2423More content."#;
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 "Unclosed fences inside HTML comments should be ignored"
2432 );
2433 }
2434
2435 #[test]
2436 fn test_multiline_html_comment_with_indented_code() {
2437 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2439 let content = r#"# Document
2440
2441<!--
2442Example:
2443
2444 indented code
2445 more code
2446
2447End of comment.
2448-->
2449
2450Regular text."#;
2451
2452 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2453 let result = rule.check(&ctx).unwrap();
2454
2455 assert_eq!(
2456 result.len(),
2457 0,
2458 "Indented code inside HTML comments should not be flagged"
2459 );
2460 }
2461
2462 #[test]
2463 fn test_code_block_after_html_comment() {
2464 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2466 let content = r#"# Document
2467
2468<!-- comment -->
2469
2470Text before.
2471
2472 indented code should be flagged
2473
2474More text."#;
2475
2476 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2477 let result = rule.check(&ctx).unwrap();
2478
2479 assert_eq!(
2480 result.len(),
2481 1,
2482 "Code blocks after HTML comments should still be detected"
2483 );
2484 assert!(result[0].message.contains("Use fenced code blocks"));
2485 }
2486
2487 #[test]
2488 fn test_consistent_style_indented_html_comment() {
2489 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2495 let content = "# MD046 false-positive reproduction\n\
2496 \n\
2497 <!--\n \
2498 This is just an indented comment, not a code block.\n\
2499 \n \
2500 A second line is required to trigger the false-positive.\n\
2501 \n \
2502 Actually, three lines are required.\n\
2503 -->\n\
2504 \n\
2505 ```md\n\
2506 This should be fine, since it's the only code block and therefore consistent.\n\
2507 ```\n";
2508
2509 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2510 let result = rule.check(&ctx).unwrap();
2511
2512 assert_eq!(
2513 result,
2514 vec![],
2515 "A single fenced block and an indented HTML comment must produce no MD046 warnings",
2516 );
2517 }
2518
2519 #[test]
2520 fn test_consistent_style_indented_html_block() {
2521 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2528 let content = "# Heading\n\
2529 \n\
2530 <div class=\"note\">\n \
2531 line one of indented html content\n \
2532 line two of indented html content\n \
2533 line three of indented html content\n\
2534 </div>\n\
2535 \n\
2536 ```md\n\
2537 real fenced block\n\
2538 ```\n";
2539
2540 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2541 let result = rule.check(&ctx).unwrap();
2542
2543 assert_eq!(
2544 result,
2545 vec![],
2546 "Indented content inside a raw HTML block must not influence MD046 style detection",
2547 );
2548 }
2549
2550 #[test]
2551 fn test_consistent_style_fake_fence_inside_html_comment() {
2552 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2558 let content = "# Title\n\
2559 \n\
2560 <!--\n\
2561 ```\n\
2562 fake fence inside comment\n\
2563 ```\n\
2564 -->\n\
2565 \n \
2566 real indented code block line 1\n \
2567 real indented code block line 2\n";
2568
2569 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2570 let result = rule.check(&ctx).unwrap();
2571
2572 assert_eq!(
2573 result,
2574 vec![],
2575 "Fence markers inside an HTML comment must not influence MD046 style detection",
2576 );
2577 }
2578
2579 #[test]
2580 fn test_consistent_style_indented_footnote_definition() {
2581 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2585 let content = "# Heading\n\
2586 \n\
2587 Reference to a footnote[^note].\n\
2588 \n\
2589 [^note]: First line of the footnote.\n \
2590 Second indented continuation line.\n \
2591 Third indented continuation line.\n \
2592 Fourth indented continuation line.\n\
2593 \n\
2594 ```md\n\
2595 real fenced block\n\
2596 ```\n";
2597
2598 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2599 let result = rule.check(&ctx).unwrap();
2600
2601 assert_eq!(
2602 result,
2603 vec![],
2604 "Footnote-definition continuation content must not influence MD046 style detection",
2605 );
2606 }
2607
2608 #[test]
2609 fn test_consistent_style_indented_blockquote() {
2610 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2615 let content = "# Heading\n\
2616 \n\
2617 > line one of quoted indented content\n\
2618 >\n\
2619 > line two of quoted indented content\n\
2620 >\n\
2621 > line three of quoted indented content\n\
2622 \n\
2623 ```md\n\
2624 real fenced block\n\
2625 ```\n";
2626
2627 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2628 let result = rule.check(&ctx).unwrap();
2629
2630 assert_eq!(
2631 result,
2632 vec![],
2633 "Indented content inside a blockquote must not influence MD046 style detection",
2634 );
2635 }
2636
2637 #[test]
2638 fn test_consistent_style_genuine_indented_block_detected_as_indented() {
2639 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2644 let content = "# Heading\n\
2645 \n\
2646 Some prose.\n\
2647 \n \
2648 real indented code line 1\n \
2649 real indented code line 2\n";
2650
2651 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2652 let result = rule.check(&ctx).unwrap();
2653
2654 assert_eq!(
2657 result,
2658 vec![],
2659 "A genuine top-level indented block must be detected as Indented style under Consistent",
2660 );
2661 }
2662
2663 #[test]
2664 fn test_consistent_style_skipped_lines_dont_override_real_block() {
2665 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2670 let content = "# Heading\n\
2671 \n\
2672 <!--\n \
2673 skipped indented comment line 1\n \
2674 skipped indented comment line 2\n\
2675 -->\n\
2676 \n\
2677 <!--\n \
2678 second skipped region\n \
2679 also skipped\n\
2680 -->\n\
2681 \n \
2682 real indented code line\n";
2683
2684 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2685 let result = rule.check(&ctx).unwrap();
2686
2687 assert_eq!(
2688 result,
2689 vec![],
2690 "Skipped container lines must not outweigh the single real indented block",
2691 );
2692 }
2693
2694 #[test]
2695 fn test_consistent_style_fenced_wins_over_skipped_indented() {
2696 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2700 let content = "# Heading\n\
2701 \n\
2702 <!--\n \
2703 skipped indented region one\n \
2704 more of region one\n\
2705 -->\n\
2706 \n\
2707 <!--\n \
2708 skipped indented region two\n \
2709 more of region two\n\
2710 -->\n\
2711 \n\
2712 ```md\n\
2713 real fenced block\n\
2714 ```\n";
2715
2716 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2717 let result = rule.check(&ctx).unwrap();
2718
2719 assert_eq!(
2720 result,
2721 vec![],
2722 "Fenced block must win when all indented lines are inside skipped containers",
2723 );
2724 }
2725
2726 #[test]
2727 fn test_four_space_indented_fence_is_not_valid_fence() {
2728 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2731
2732 assert!(rule.is_fenced_code_block_start("```"));
2734 assert!(rule.is_fenced_code_block_start(" ```"));
2735 assert!(rule.is_fenced_code_block_start(" ```"));
2736 assert!(rule.is_fenced_code_block_start(" ```"));
2737
2738 assert!(!rule.is_fenced_code_block_start(" ```"));
2740 assert!(!rule.is_fenced_code_block_start(" ```"));
2741 assert!(!rule.is_fenced_code_block_start(" ```"));
2742
2743 assert!(!rule.is_fenced_code_block_start("\t```"));
2745 }
2746
2747 #[test]
2748 fn test_issue_237_indented_fenced_block_detected_as_indented() {
2749 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2755
2756 let content = r#"## Test
2758
2759 ```js
2760 var foo = "hello";
2761 ```
2762"#;
2763
2764 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2765 let result = rule.check(&ctx).unwrap();
2766
2767 assert_eq!(
2769 result.len(),
2770 1,
2771 "4-space indented fence should be detected as indented code block"
2772 );
2773 assert!(
2774 result[0].message.contains("Use fenced code blocks"),
2775 "Expected 'Use fenced code blocks' message"
2776 );
2777 }
2778
2779 #[test]
2780 fn test_issue_276_indented_code_in_list() {
2781 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2784
2785 let content = r#"1. First item
27862. Second item with code:
2787
2788 # This is a code block in a list
2789 print("Hello, world!")
2790
27914. Third item"#;
2792
2793 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2794 let result = rule.check(&ctx).unwrap();
2795
2796 assert!(
2798 !result.is_empty(),
2799 "Indented code block inside list should be flagged when style=fenced"
2800 );
2801 assert!(
2802 result[0].message.contains("Use fenced code blocks"),
2803 "Expected 'Use fenced code blocks' message"
2804 );
2805 }
2806
2807 #[test]
2808 fn test_three_space_indented_fence_is_valid() {
2809 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2811
2812 let content = r#"## Test
2813
2814 ```js
2815 var foo = "hello";
2816 ```
2817"#;
2818
2819 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2820 let result = rule.check(&ctx).unwrap();
2821
2822 assert_eq!(
2824 result.len(),
2825 0,
2826 "3-space indented fence should be recognized as valid fenced code block"
2827 );
2828 }
2829
2830 #[test]
2831 fn test_indented_style_with_deeply_indented_fenced() {
2832 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2835
2836 let content = r#"Text
2837
2838 ```js
2839 var foo = "hello";
2840 ```
2841
2842More text
2843"#;
2844
2845 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2846 let result = rule.check(&ctx).unwrap();
2847
2848 assert_eq!(
2851 result.len(),
2852 0,
2853 "4-space indented content should be valid when style=indented"
2854 );
2855 }
2856
2857 #[test]
2858 fn test_fix_misplaced_fenced_block() {
2859 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2862
2863 let content = r#"## Test
2864
2865 ```js
2866 var foo = "hello";
2867 ```
2868"#;
2869
2870 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2871 let fixed = rule.fix(&ctx).unwrap();
2872
2873 let expected = r#"## Test
2875
2876```js
2877var foo = "hello";
2878```
2879"#;
2880
2881 assert_eq!(fixed, expected, "Fix should remove indentation, not add more fences");
2882 }
2883
2884 #[test]
2885 fn test_fix_regular_indented_block() {
2886 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2889
2890 let content = r#"Text
2891
2892 var foo = "hello";
2893 console.log(foo);
2894
2895More text
2896"#;
2897
2898 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2899 let fixed = rule.fix(&ctx).unwrap();
2900
2901 assert!(fixed.contains("```\nvar foo"), "Should add opening fence");
2903 assert!(fixed.contains("console.log(foo);\n```"), "Should add closing fence");
2904 }
2905
2906 #[test]
2907 fn test_fix_indented_block_with_fence_like_content() {
2908 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2912
2913 let content = r#"Text
2914
2915 some code
2916 ```not a fence opener
2917 more code
2918"#;
2919
2920 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2921 let fixed = rule.fix(&ctx).unwrap();
2922
2923 assert!(fixed.contains(" some code"), "Unsafe block should be left unchanged");
2925 assert!(!fixed.contains("```\nsome code"), "Should NOT wrap unsafe block");
2926 }
2927
2928 #[test]
2929 fn test_fix_mixed_indented_and_misplaced_blocks() {
2930 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2932
2933 let content = r#"Text
2934
2935 regular indented code
2936
2937More text
2938
2939 ```python
2940 print("hello")
2941 ```
2942"#;
2943
2944 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2945 let fixed = rule.fix(&ctx).unwrap();
2946
2947 assert!(
2949 fixed.contains("```\nregular indented code\n```"),
2950 "First block should be wrapped in fences"
2951 );
2952
2953 assert!(
2955 fixed.contains("\n```python\nprint(\"hello\")\n```"),
2956 "Second block should be dedented, not double-wrapped"
2957 );
2958 assert!(
2960 !fixed.contains("```\n```python"),
2961 "Should not have nested fence openers"
2962 );
2963 }
2964
2965 #[test]
2966 fn test_md046_front_matter() {
2967 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2968 let content = "---\nmetadata:\n\n description: Indented\n---\n";
2969 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2970 let result = rule.check(&ctx).unwrap();
2971 assert_eq!(result.len(), 0);
2972 }
2973
2974 #[test]
2975 fn test_md046_fix_front_matter() {
2976 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2977 let content = "---\nmetadata:\n\n description: Indented\n---\n";
2978 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2979 let fixed = rule.fix(&ctx).unwrap();
2980 assert_eq!(fixed, content);
2981 }
2982
2983 #[test]
2984 fn test_whitespace_only_line_is_not_an_indented_code_block() {
2985 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2990 let content = "# T\n\nPara\n\n \nMore\n\n real code\n\nEnd\n";
2991 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2992 let fixed = rule.fix(&ctx).unwrap();
2993 assert_eq!(fixed, "# T\n\nPara\n\n \nMore\n\n```\nreal code\n```\n\nEnd\n");
2994 }
2995
2996 #[test]
2997 fn test_interior_blank_line_keeps_indented_block_together() {
2998 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3002 let content = "# T\n\nPara\n\n a\n\n b\n\nAfter\n";
3003 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3004 let fixed = rule.fix(&ctx).unwrap();
3005 assert_eq!(fixed, "# T\n\nPara\n\n```\na\n\nb\n```\n\nAfter\n");
3006 }
3007
3008 #[test]
3009 fn test_consistent_style_counts_a_block_with_interior_blank_once() {
3010 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3014 let content = "# T\n\n```\nfenced\n```\n\nPara\n\n a\n\n b\n\nEnd\n";
3015 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3016 let result = rule.check(&ctx).unwrap();
3017 let reported: Vec<(usize, &str)> = result.iter().map(|w| (w.line, w.message.as_str())).collect();
3018 assert_eq!(reported, vec![(9, "Use fenced code blocks")]);
3019 }
3020
3021 #[test]
3022 fn test_indented_lazy_continuation_lines_are_not_code() {
3023 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3029 let content = "# T\n\nPara\n lazy one\n lazy two\n lazy three\n\n real code\n\nEnd\n";
3030 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3031 let fixed = rule.fix(&ctx).unwrap();
3032 assert_eq!(
3033 fixed,
3034 "# T\n\nPara\n lazy one\n lazy two\n lazy three\n\n```\nreal code\n```\n\nEnd\n"
3035 );
3036 }
3037
3038 #[test]
3039 fn test_misplaced_fence_with_interior_blank_dedents_as_one_block() {
3040 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3044 let content = "# T\n\nPara\n\n ```python\n x = 1\n\n y = 2\n ```\n\nAfter\n";
3045 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3046 let fixed = rule.fix(&ctx).unwrap();
3047 assert_eq!(fixed, "# T\n\nPara\n\n```python\nx = 1\n\ny = 2\n```\n\nAfter\n");
3048 }
3049}