1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::rule_config_serde::{FlavorOverrideNotice, option_is_explicit};
3use crate::utils::calculate_indentation_width_default;
4use crate::utils::mdg;
5use crate::utils::mkdocs_admonitions;
6use crate::utils::mkdocs_tabs;
7use crate::utils::range_utils::calculate_line_range;
8use toml;
9
10mod md046_config;
11pub use md046_config::CodeBlockStyle;
12use md046_config::MD046Config;
13
14static MDG_STYLE_OVERRIDE: FlavorOverrideNotice = FlavorOverrideNotice::new();
16
17struct IndentContext<'a> {
19 in_list_context: &'a [bool],
20 in_tab_context: &'a [bool],
21 in_admonition_context: &'a [bool],
22 in_comment_or_html: &'a [bool],
32 list_item_baseline: &'a [Option<usize>],
42}
43
44struct OwnedIndentContext {
47 in_list_context: Vec<bool>,
48 in_tab_context: Vec<bool>,
49 in_admonition_context: Vec<bool>,
50 in_comment_or_html: Vec<bool>,
51 list_item_baseline: Vec<Option<usize>>,
52}
53
54impl OwnedIndentContext {
55 fn borrow(&self) -> IndentContext<'_> {
56 IndentContext {
57 in_list_context: &self.in_list_context,
58 in_tab_context: &self.in_tab_context,
59 in_admonition_context: &self.in_admonition_context,
60 in_comment_or_html: &self.in_comment_or_html,
61 list_item_baseline: &self.list_item_baseline,
62 }
63 }
64}
65
66#[derive(Clone)]
72pub struct MD046CodeBlockStyle {
73 config: MD046Config,
74 style_explicit: bool,
78}
79
80impl MD046CodeBlockStyle {
81 const FENCE: &'static str = "```";
83
84 pub fn new(style: CodeBlockStyle) -> Self {
85 Self {
86 config: MD046Config { style },
87 style_explicit: true,
88 }
89 }
90
91 pub fn from_config_struct(config: MD046Config) -> Self {
92 Self {
93 config,
94 style_explicit: false,
95 }
96 }
97
98 fn has_valid_fence_indent(line: &str) -> bool {
103 calculate_indentation_width_default(line) < 4
104 }
105
106 fn is_fenced_code_block_start(&self, line: &str) -> bool {
115 if !Self::has_valid_fence_indent(line) {
116 return false;
117 }
118
119 let trimmed = line.trim_start();
120 trimmed.starts_with("```") || trimmed.starts_with("~~~")
121 }
122
123 fn is_list_item(&self, line: &str) -> bool {
124 let trimmed = line.trim_start();
125 if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ") {
126 return true;
127 }
128 let after_digits = trimmed.trim_start_matches(|c: char| c.is_ascii_digit());
133 after_digits.len() < trimmed.len() && (after_digits.starts_with(". ") || after_digits.starts_with(") "))
134 }
135
136 fn is_footnote_definition(&self, line: &str) -> bool {
156 let trimmed = line.trim_start();
157 if !trimmed.starts_with("[^") || trimmed.len() < 5 {
158 return false;
159 }
160
161 if let Some(close_bracket_pos) = trimmed.find("]:")
162 && close_bracket_pos > 2
163 {
164 let label = &trimmed[2..close_bracket_pos];
165
166 if label.trim().is_empty() {
167 return false;
168 }
169
170 if label.contains('\r') {
172 return false;
173 }
174
175 if label.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
177 return true;
178 }
179 }
180
181 false
182 }
183
184 fn precompute_block_continuation_context(&self, lines: &[&str]) -> Vec<bool> {
207 let mut in_continuation_context = vec![false; lines.len()];
208 let mut last_list_item_line: Option<usize> = None;
209 let mut last_footnote_line: Option<usize> = None;
210 let mut blank_line_count = 0;
211
212 for (i, line) in lines.iter().enumerate() {
213 let trimmed = line.trim_start();
214 let indent_len = line.len() - trimmed.len();
215
216 if self.is_list_item(line) {
218 last_list_item_line = Some(i);
219 last_footnote_line = None; blank_line_count = 0;
221 in_continuation_context[i] = true;
222 continue;
223 }
224
225 if self.is_footnote_definition(line) {
227 last_footnote_line = Some(i);
228 last_list_item_line = None; blank_line_count = 0;
230 in_continuation_context[i] = true;
231 continue;
232 }
233
234 if line.trim().is_empty() {
236 if last_list_item_line.is_some() || last_footnote_line.is_some() {
238 blank_line_count += 1;
239 in_continuation_context[i] = true;
240
241 }
245 continue;
246 }
247
248 if indent_len == 0 && !trimmed.is_empty() {
250 if trimmed.starts_with('#') {
254 last_list_item_line = None;
255 last_footnote_line = None;
256 blank_line_count = 0;
257 continue;
258 }
259
260 if trimmed.starts_with("---") || trimmed.starts_with("***") {
262 last_list_item_line = None;
263 last_footnote_line = None;
264 blank_line_count = 0;
265 continue;
266 }
267
268 if let Some(list_line) = last_list_item_line
271 && (i - list_line > 5 || blank_line_count > 1)
272 {
273 last_list_item_line = None;
274 }
275
276 if last_footnote_line.is_some() {
278 last_footnote_line = None;
279 }
280
281 blank_line_count = 0;
282
283 if last_list_item_line.is_none() && last_footnote_line.is_some() {
285 last_footnote_line = None;
286 }
287 continue;
288 }
289
290 if indent_len > 0 && (last_list_item_line.is_some() || last_footnote_line.is_some()) {
292 in_continuation_context[i] = true;
293 blank_line_count = 0;
294 }
295 }
296
297 in_continuation_context
298 }
299
300 fn precompute_list_item_baseline(
311 &self,
312 ctx: &crate::lint_context::LintContext,
313 lines: &[&str],
314 ) -> Vec<Option<usize>> {
315 let mut baselines = vec![None; lines.len()];
316 let mut last_baseline: Option<usize> = None;
317 let mut last_list_item_line: Option<usize> = None;
318 let mut blank_line_count = 0usize;
319
320 for (i, line) in lines.iter().enumerate() {
321 let trimmed = line.trim_start();
322 let indent_len = line.len() - trimmed.len();
323
324 if let Some(item) = ctx.line_info(i + 1).and_then(|li| li.list_item.as_ref()) {
326 last_baseline = Some(item.content_column);
327 last_list_item_line = Some(i);
328 blank_line_count = 0;
329 baselines[i] = last_baseline;
330 continue;
331 }
332
333 if line.trim().is_empty() {
335 if last_baseline.is_some() {
336 blank_line_count += 1;
337 baselines[i] = last_baseline;
338 }
339 continue;
340 }
341
342 if indent_len == 0 {
346 if trimmed.starts_with('#') || trimmed.starts_with("---") || trimmed.starts_with("***") {
347 last_baseline = None;
348 last_list_item_line = None;
349 } else if let Some(list_line) = last_list_item_line
350 && (i - list_line > 5 || blank_line_count > 1)
351 {
352 last_baseline = None;
353 last_list_item_line = None;
354 }
355 blank_line_count = 0;
356 continue;
357 }
358
359 if last_baseline.is_some() {
361 baselines[i] = last_baseline;
362 blank_line_count = 0;
363 }
364 }
365
366 baselines
367 }
368
369 fn is_indented_code_block_with_context(
373 &self,
374 lines: &[&str],
375 i: usize,
376 is_mkdocs: bool,
377 ctx: &IndentContext,
378 prev_is_code: bool,
379 ) -> bool {
380 if i >= lines.len() {
381 return false;
382 }
383
384 let line = lines[i];
385
386 if line.trim().is_empty() {
391 return false;
392 }
393
394 let indent = calculate_indentation_width_default(line);
396 if indent < 4 {
397 return false;
398 }
399
400 if ctx.in_list_context[i] {
406 let crosses_baseline = ctx
407 .list_item_baseline
408 .get(i)
409 .copied()
410 .flatten()
411 .is_some_and(|base| indent >= base + 4);
412 if !crosses_baseline {
413 return false;
414 }
415 }
416
417 if is_mkdocs && ctx.in_tab_context[i] {
419 return false;
420 }
421
422 if is_mkdocs && ctx.in_admonition_context[i] {
425 return false;
426 }
427
428 if ctx.in_comment_or_html.get(i).copied().unwrap_or(false) {
434 return false;
435 }
436
437 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
445 has_blank_line_before || prev_is_code
446 }
447
448 fn first_code_block_line(
457 ctx: &crate::lint_context::LintContext,
458 block_lines: &[bool],
459 start: usize,
460 block_end: usize,
461 ) -> Option<usize> {
462 (start..block_lines.len())
463 .take_while(|&idx| ctx.line_offsets.get(idx).is_some_and(|&offset| offset < block_end))
464 .find(|&idx| block_lines[idx])
465 }
466
467 fn indented_block_lines(
483 &self,
484 lines: &[&str],
485 is_mkdocs: bool,
486 ictx: &IndentContext<'_>,
487 flavor: crate::config::MarkdownFlavor,
488 ) -> Vec<bool> {
489 let mut member = vec![false; lines.len()];
490 for i in 0..lines.len() {
491 let prev_is_code = i > 0 && member[i - 1];
492 member[i] = self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx, prev_is_code);
493 }
494
495 if flavor == crate::config::MarkdownFlavor::MDG {
502 let mut i = 0;
503 while i < member.len() {
504 if !member[i] {
505 i += 1;
506 continue;
507 }
508 let start = i;
509 while i < member.len() && member[i] {
510 i += 1;
511 }
512 if lines[start..i].iter().all(|line| mdg::is_table_row(line)) {
513 member[start..i].fill(false);
514 }
515 }
516 }
517
518 let mut i = 0;
519 while i < lines.len() {
520 if !member[i] {
521 i += 1;
522 continue;
523 }
524 let mut next = i + 1;
525 while next < lines.len() && lines[next].trim().is_empty() {
526 next += 1;
527 }
528 if next < lines.len() && member[next] {
529 member[i + 1..next].fill(true);
530 }
531 i = next;
532 }
533
534 member
535 }
536
537 fn precompute_comment_or_html_context(ctx: &crate::lint_context::LintContext, line_count: usize) -> Vec<bool> {
546 (0..line_count)
547 .map(|i| {
548 ctx.line_info(i + 1).is_some_and(|info| {
549 info.in_html_comment
550 || info.in_mdx_comment
551 || info.in_html_block
552 || info.in_jsx_block
553 || info.in_mkdocstrings
554 || info.in_footnote_definition
555 || info.blockquote.is_some()
556 || info.in_front_matter
557 })
558 })
559 .collect()
560 }
561
562 fn precompute_mkdocs_tab_context(&self, lines: &[&str]) -> Vec<bool> {
564 let mut in_tab_context = vec![false; lines.len()];
565 let mut current_tab_indent: Option<usize> = None;
566
567 for (i, line) in lines.iter().enumerate() {
568 if mkdocs_tabs::is_tab_marker(line) {
570 let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
571 current_tab_indent = Some(tab_indent);
572 in_tab_context[i] = true;
573 continue;
574 }
575
576 if let Some(tab_indent) = current_tab_indent {
578 if mkdocs_tabs::is_tab_content(line, tab_indent) {
579 in_tab_context[i] = true;
580 } else if !line.trim().is_empty() && calculate_indentation_width_default(line) < 4 {
581 current_tab_indent = None;
583 } else {
584 in_tab_context[i] = true;
586 }
587 }
588 }
589
590 in_tab_context
591 }
592
593 fn precompute_mkdocs_admonition_context(&self, lines: &[&str]) -> Vec<bool> {
602 let mut in_admonition_context = vec![false; lines.len()];
603 let mut admonition_stack: Vec<usize> = Vec::new();
605
606 for (i, line) in lines.iter().enumerate() {
607 let line_indent = calculate_indentation_width_default(line);
608
609 if mkdocs_admonitions::is_admonition_start(line) {
611 let adm_indent = mkdocs_admonitions::get_admonition_indent(line).unwrap_or(0);
612
613 while let Some(&top_indent) = admonition_stack.last() {
615 if adm_indent <= top_indent {
617 admonition_stack.pop();
618 } else {
619 break;
620 }
621 }
622
623 admonition_stack.push(adm_indent);
625 in_admonition_context[i] = true;
626 continue;
627 }
628
629 if line.trim().is_empty() {
631 if !admonition_stack.is_empty() {
632 in_admonition_context[i] = true;
633 }
634 continue;
635 }
636
637 while let Some(&top_indent) = admonition_stack.last() {
640 if line_indent >= top_indent + 4 {
642 break;
644 } else {
645 admonition_stack.pop();
647 }
648 }
649
650 if !admonition_stack.is_empty() {
652 in_admonition_context[i] = true;
653 }
654 }
655
656 in_admonition_context
657 }
658
659 fn build_indent_context(
671 &self,
672 ctx: &crate::lint_context::LintContext,
673 lines: &[&str],
674 is_mkdocs: bool,
675 ) -> OwnedIndentContext {
676 OwnedIndentContext {
677 in_list_context: self.precompute_block_continuation_context(lines),
678 in_tab_context: if is_mkdocs {
679 self.precompute_mkdocs_tab_context(lines)
680 } else {
681 vec![false; lines.len()]
682 },
683 in_admonition_context: if is_mkdocs {
684 self.precompute_mkdocs_admonition_context(lines)
685 } else {
686 vec![false; lines.len()]
687 },
688 in_comment_or_html: Self::precompute_comment_or_html_context(ctx, lines.len()),
689 list_item_baseline: self.precompute_list_item_baseline(ctx, lines),
690 }
691 }
692
693 fn categorize_indented_blocks(&self, lines: &[&str], block_lines: &[bool]) -> (Vec<bool>, Vec<bool>) {
705 let mut is_misplaced = vec![false; lines.len()];
706 let mut contains_fences = vec![false; lines.len()];
707
708 let mut i = 0;
710 while i < lines.len() {
711 if !block_lines[i] {
713 i += 1;
714 continue;
715 }
716
717 let block_start = i;
719 let mut block_end = i;
720
721 while block_end < lines.len() && block_lines[block_end] {
722 block_end += 1;
723 }
724
725 if block_end > block_start {
727 let first_line = lines[block_start].trim_start();
728 let last_line = lines[block_end - 1].trim_start();
729
730 let is_backtick_fence = first_line.starts_with("```");
732 let is_tilde_fence = first_line.starts_with("~~~");
733
734 if is_backtick_fence || is_tilde_fence {
735 let fence_char = if is_backtick_fence { '`' } else { '~' };
736 let opener_len = first_line.chars().take_while(|&c| c == fence_char).count();
737
738 let closer_fence_len = last_line.chars().take_while(|&c| c == fence_char).count();
740 let after_closer = &last_line[closer_fence_len..];
741
742 if closer_fence_len >= opener_len && after_closer.trim().is_empty() {
743 is_misplaced[block_start..block_end].fill(true);
745 } else {
746 contains_fences[block_start..block_end].fill(true);
748 }
749 } else {
750 let has_fence_markers = (block_start..block_end).any(|j| {
753 let trimmed = lines[j].trim_start();
754 trimmed.starts_with("```") || trimmed.starts_with("~~~")
755 });
756
757 if has_fence_markers {
758 contains_fences[block_start..block_end].fill(true);
759 }
760 }
761 }
762
763 i = block_end;
764 }
765
766 (is_misplaced, contains_fences)
767 }
768
769 fn check_unclosed_code_blocks(&self, ctx: &crate::lint_context::LintContext) -> Vec<LintWarning> {
770 let mut warnings = Vec::new();
771 let lines = ctx.raw_lines();
772
773 let has_markdown_doc_block = ctx.code_block_details.iter().any(|d| {
775 if !d.is_fenced {
776 return false;
777 }
778 let lang = d.info_string.to_lowercase();
779 lang.starts_with("markdown") || lang.starts_with("md")
780 });
781
782 if has_markdown_doc_block {
785 return warnings;
786 }
787
788 for detail in &ctx.code_block_details {
789 if !detail.is_fenced {
790 continue;
791 }
792
793 if detail.end != ctx.content.len() {
795 continue;
796 }
797
798 let opening_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
800 Ok(idx) => idx,
801 Err(idx) => idx.saturating_sub(1),
802 };
803
804 let line = lines.get(opening_line_idx).unwrap_or(&"");
806 let trimmed = line.trim();
807 let fence_marker = if let Some(pos) = trimmed.find("```") {
808 let count = trimmed[pos..].chars().take_while(|&c| c == '`').count();
809 "`".repeat(count)
810 } else if let Some(pos) = trimmed.find("~~~") {
811 let count = trimmed[pos..].chars().take_while(|&c| c == '~').count();
812 "~".repeat(count)
813 } else {
814 "```".to_string()
815 };
816
817 let last_non_empty_line = lines.iter().rev().find(|l| !l.trim().is_empty()).unwrap_or(&"");
819 let last_trimmed = last_non_empty_line.trim();
820 let fence_char = fence_marker.chars().next().unwrap_or('`');
821
822 let has_closing_fence = if fence_char == '`' {
823 last_trimmed.starts_with("```") && {
824 let fence_len = last_trimmed.chars().take_while(|&c| c == '`').count();
825 last_trimmed[fence_len..].trim().is_empty()
826 }
827 } else {
828 last_trimmed.starts_with("~~~") && {
829 let fence_len = last_trimmed.chars().take_while(|&c| c == '~').count();
830 last_trimmed[fence_len..].trim().is_empty()
831 }
832 };
833
834 if !has_closing_fence {
835 if ctx
837 .lines
838 .get(opening_line_idx)
839 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
840 {
841 continue;
842 }
843
844 let (start_line, start_col, end_line, end_col) = calculate_line_range(opening_line_idx + 1, line);
845
846 warnings.push(LintWarning {
847 rule_name: Some(self.name().to_string()),
848 line: start_line,
849 column: start_col,
850 end_line,
851 end_column: end_col,
852 message: format!("Code block opened with '{fence_marker}' but never closed"),
853 severity: Severity::Warning,
854 fix: Some(Fix::new(
855 ctx.content.len()..ctx.content.len(),
856 format!("\n{fence_marker}"),
857 )),
858 });
859 }
860 }
861
862 warnings
863 }
864
865 fn effective_target_style(
873 &self,
874 flavor: crate::config::MarkdownFlavor,
875 detect: impl FnOnce() -> CodeBlockStyle,
876 ) -> CodeBlockStyle {
877 if flavor == crate::config::MarkdownFlavor::MDG {
878 self.warn_once_about_overridden_style();
879 return CodeBlockStyle::Fenced;
880 }
881
882 match self.config.style {
883 CodeBlockStyle::Consistent => detect(),
884 style => style,
885 }
886 }
887
888 fn warn_once_about_overridden_style(&self) {
894 if !self.style_explicit || self.config.style != CodeBlockStyle::Indented {
895 return;
896 }
897
898 MDG_STYLE_OVERRIDE.report(
899 "MD046",
900 "style",
901 "indented",
902 "fenced",
903 "a Gherkin Doc String is only ever a backtick fence",
904 );
905 }
906
907 fn detect_style(
908 &self,
909 ctx: &crate::lint_context::LintContext,
910 lines: &[&str],
911 is_mkdocs: bool,
912 ictx: &IndentContext,
913 ) -> Option<CodeBlockStyle> {
914 if lines.is_empty() {
915 return None;
916 }
917
918 let block_lines = self.indented_block_lines(lines, is_mkdocs, ictx, ctx.flavor);
919
920 let mut fenced_count = 0;
921 let mut indented_count = 0;
922
923 let mut in_fenced = false;
933 let mut prev_was_indented = false;
934
935 for (i, line) in lines.iter().enumerate() {
936 let in_container = ictx.in_comment_or_html.get(i).copied().unwrap_or(false);
937
938 if ctx.flavor.supports_colon_code_fences() && ctx.lines.get(i).is_some_and(|l| l.in_code_block) {
942 prev_was_indented = false;
943 continue;
944 }
945
946 if ctx.flavor.supports_myst_directives() && ctx.lines.get(i).is_some_and(|l| l.in_myst_directive) {
948 prev_was_indented = false;
949 continue;
950 }
951
952 if self.is_fenced_code_block_start(line) {
953 if in_container {
954 prev_was_indented = false;
957 continue;
958 }
959 if !in_fenced {
960 fenced_count += 1;
962 in_fenced = true;
963 } else {
964 in_fenced = false;
966 }
967 prev_was_indented = false;
968 } else if !in_fenced && block_lines[i] {
969 if !prev_was_indented {
971 indented_count += 1;
972 }
973 prev_was_indented = true;
974 } else {
975 prev_was_indented = false;
976 }
977 }
978
979 if fenced_count == 0 && indented_count == 0 {
980 None
981 } else if fenced_count > 0 && indented_count == 0 {
982 Some(CodeBlockStyle::Fenced)
983 } else if fenced_count == 0 && indented_count > 0 {
984 Some(CodeBlockStyle::Indented)
985 } else if fenced_count >= indented_count {
986 Some(CodeBlockStyle::Fenced)
987 } else {
988 Some(CodeBlockStyle::Indented)
989 }
990 }
991}
992
993impl Rule for MD046CodeBlockStyle {
994 fn name(&self) -> &'static str {
995 "MD046"
996 }
997
998 fn description(&self) -> &'static str {
999 "Code blocks should use a consistent style"
1000 }
1001
1002 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
1003 if ctx.content.is_empty() {
1005 return Ok(Vec::new());
1006 }
1007
1008 if !ctx.content.contains("```")
1010 && !ctx.content.contains("~~~")
1011 && !ctx.content.contains(" ")
1012 && !ctx.content.contains('\t')
1013 {
1014 return Ok(Vec::new());
1015 }
1016
1017 let unclosed_warnings = self.check_unclosed_code_blocks(ctx);
1019
1020 if !unclosed_warnings.is_empty() {
1022 return Ok(unclosed_warnings);
1023 }
1024
1025 let lines = ctx.raw_lines();
1027 let mut warnings = Vec::new();
1028
1029 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
1030
1031 let target_style = self.effective_target_style(ctx.flavor, || {
1033 let owned = self.build_indent_context(ctx, lines, is_mkdocs);
1034 let detected = self.detect_style(ctx, lines, is_mkdocs, &owned.borrow());
1035 detected.unwrap_or(CodeBlockStyle::Fenced)
1036 });
1037
1038 let mdg_block_lines = (ctx.flavor == crate::config::MarkdownFlavor::MDG
1043 && ctx.code_block_details.iter().any(|detail| !detail.is_fenced))
1044 .then(|| {
1045 let owned = self.build_indent_context(ctx, lines, is_mkdocs);
1046 self.indented_block_lines(lines, is_mkdocs, &owned.borrow(), ctx.flavor)
1047 });
1048
1049 let mut reported_indented_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
1051
1052 for detail in &ctx.code_block_details {
1053 if detail.start >= ctx.content.len() || detail.end > ctx.content.len() {
1054 continue;
1055 }
1056
1057 let start_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
1058 Ok(idx) => idx,
1059 Err(idx) => idx.saturating_sub(1),
1060 };
1061
1062 if detail.is_fenced {
1063 if target_style == CodeBlockStyle::Indented {
1064 let line = lines.get(start_line_idx).unwrap_or(&"");
1065
1066 if ctx
1067 .lines
1068 .get(start_line_idx)
1069 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment || info.in_footnote_definition)
1070 {
1071 continue;
1072 }
1073
1074 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
1075 warnings.push(LintWarning {
1076 rule_name: Some(self.name().to_string()),
1077 line: start_line,
1078 column: start_col,
1079 end_line,
1080 end_column: end_col,
1081 message: "Use indented code blocks".to_string(),
1082 severity: Severity::Warning,
1083 fix: None,
1084 });
1085 }
1086 } else {
1087 if target_style == CodeBlockStyle::Fenced {
1089 let start_line_idx = match &mdg_block_lines {
1094 Some(block_lines) => {
1095 match Self::first_code_block_line(ctx, block_lines, start_line_idx, detail.end) {
1096 Some(idx) => idx,
1097 None => continue,
1098 }
1099 }
1100 None => start_line_idx,
1101 };
1102
1103 if reported_indented_lines.contains(&start_line_idx) {
1104 continue;
1105 }
1106
1107 let line = lines.get(start_line_idx).unwrap_or(&"");
1108
1109 if ctx.lines.get(start_line_idx).is_some_and(|info| {
1111 info.in_html_comment
1112 || info.in_mdx_comment
1113 || info.in_html_block
1114 || info.in_jsx_block
1115 || info.in_mkdocstrings
1116 || info.in_footnote_definition
1117 || info.blockquote.is_some()
1118 || info.in_front_matter
1119 }) {
1120 continue;
1121 }
1122
1123 if is_mkdocs
1125 && ctx
1126 .lines
1127 .get(start_line_idx)
1128 .is_some_and(|info| info.in_admonition || info.in_content_tab)
1129 {
1130 continue;
1131 }
1132
1133 reported_indented_lines.insert(start_line_idx);
1134
1135 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
1136 warnings.push(LintWarning {
1137 rule_name: Some(self.name().to_string()),
1138 line: start_line,
1139 column: start_col,
1140 end_line,
1141 end_column: end_col,
1142 message: "Use fenced code blocks".to_string(),
1143 severity: Severity::Warning,
1144 fix: None,
1145 });
1146 }
1147 }
1148 }
1149
1150 warnings.sort_by_key(|w| (w.line, w.column));
1152
1153 Ok(warnings)
1154 }
1155
1156 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
1157 let content = ctx.content;
1158 if content.is_empty() {
1159 return Ok(String::new());
1160 }
1161
1162 let lines = ctx.raw_lines();
1163
1164 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
1166
1167 let owned = self.build_indent_context(ctx, lines, is_mkdocs);
1168 let ictx = owned.borrow();
1169
1170 let target_style = self.effective_target_style(ctx.flavor, || {
1175 self.detect_style(ctx, lines, is_mkdocs, &ictx)
1176 .unwrap_or(CodeBlockStyle::Fenced)
1177 });
1178
1179 let block_lines = self.indented_block_lines(lines, is_mkdocs, &ictx, ctx.flavor);
1180
1181 let (misplaced_fence_lines, unsafe_fence_lines) = self.categorize_indented_blocks(lines, &block_lines);
1185
1186 let mut result = String::with_capacity(content.len());
1187 let mut in_fenced_block = false;
1188 let mut fenced_fence_opener: Option<(char, usize)> = None;
1192 let mut in_indented_block = false;
1193 let mut current_block_fence_indent = String::new();
1198
1199 let mut current_block_disabled = false;
1201
1202 for (i, line) in lines.iter().enumerate() {
1203 let line_num = i + 1;
1204 let trimmed = line.trim_start();
1205
1206 if !in_fenced_block
1209 && Self::has_valid_fence_indent(line)
1210 && (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
1211 {
1212 current_block_disabled = ctx.inline_config().is_rule_disabled(self.name(), line_num);
1214 in_fenced_block = true;
1215 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
1216 let opener_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1217 fenced_fence_opener = Some((fence_char, opener_len));
1218
1219 if current_block_disabled {
1220 result.push_str(line);
1222 result.push('\n');
1223 } else if target_style == CodeBlockStyle::Indented {
1224 in_indented_block = true;
1226 } else {
1227 result.push_str(line);
1229 result.push('\n');
1230 }
1231 } else if in_fenced_block && fenced_fence_opener.is_some() {
1232 let (fence_char, opener_len) = fenced_fence_opener.unwrap();
1233 let closer_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1236 let after_closer = &trimmed[closer_len..];
1237 let is_closer = closer_len >= opener_len && after_closer.trim().is_empty() && closer_len > 0;
1238 if is_closer {
1239 in_fenced_block = false;
1240 fenced_fence_opener = None;
1241 in_indented_block = false;
1242
1243 if current_block_disabled {
1244 result.push_str(line);
1245 result.push('\n');
1246 } else if target_style == CodeBlockStyle::Indented {
1247 } else {
1249 result.push_str(line);
1251 result.push('\n');
1252 }
1253 current_block_disabled = false;
1254 } else if current_block_disabled {
1255 result.push_str(line);
1257 result.push('\n');
1258 } else if target_style == CodeBlockStyle::Indented {
1259 if !line.is_empty() {
1266 result.push_str(" ");
1267 result.push_str(line);
1268 }
1269 result.push('\n');
1270 } else {
1271 result.push_str(line);
1273 result.push('\n');
1274 }
1275 } else if block_lines[i] {
1276 if ctx.inline_config().is_rule_disabled(self.name(), line_num) {
1280 result.push_str(line);
1281 result.push('\n');
1282 continue;
1283 }
1284
1285 let prev_line_is_indented = i > 0 && block_lines[i - 1];
1287
1288 if target_style == CodeBlockStyle::Fenced {
1289 let baseline = ictx.list_item_baseline.get(i).copied().flatten().unwrap_or(0);
1295 let body = if line.trim().is_empty() {
1303 ""
1304 } else {
1305 line.strip_prefix(" ").unwrap_or(line)
1306 };
1307
1308 if misplaced_fence_lines[i] {
1311 result.push_str(line.trim_start());
1313 result.push('\n');
1314 } else if unsafe_fence_lines[i] {
1315 result.push_str(line);
1318 result.push('\n');
1319 } else if !prev_line_is_indented && !in_indented_block {
1320 current_block_fence_indent = " ".repeat(baseline);
1322 result.push_str(¤t_block_fence_indent);
1323 result.push_str(Self::FENCE);
1324 result.push('\n');
1325 result.push_str(body);
1326 result.push('\n');
1327 in_indented_block = true;
1328 } else {
1329 result.push_str(body);
1331 result.push('\n');
1332 }
1333
1334 let next_line_is_indented = i < lines.len() - 1 && block_lines[i + 1];
1336 if !next_line_is_indented
1338 && in_indented_block
1339 && !misplaced_fence_lines[i]
1340 && !unsafe_fence_lines[i]
1341 {
1342 result.push_str(¤t_block_fence_indent);
1343 result.push_str(Self::FENCE);
1344 result.push('\n');
1345 in_indented_block = false;
1346 current_block_fence_indent.clear();
1347 }
1348 } else {
1349 result.push_str(line);
1351 result.push('\n');
1352 }
1353 } else {
1354 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1356 result.push_str(¤t_block_fence_indent);
1357 result.push_str(Self::FENCE);
1358 result.push('\n');
1359 in_indented_block = false;
1360 current_block_fence_indent.clear();
1361 }
1362
1363 result.push_str(line);
1364 result.push('\n');
1365 }
1366 }
1367
1368 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1370 result.push_str(¤t_block_fence_indent);
1371 result.push_str(Self::FENCE);
1372 result.push('\n');
1373 }
1374
1375 if let Some((fence_char, opener_len)) = fenced_fence_opener
1381 && in_fenced_block
1382 {
1383 let has_unclosed_violation = !self.check_unclosed_code_blocks(ctx).is_empty();
1384 if has_unclosed_violation {
1385 let closer: String = std::iter::repeat_n(fence_char, opener_len).collect();
1386 result.push_str(&closer);
1387 result.push('\n');
1388 }
1389 }
1390
1391 if !content.ends_with('\n') && result.ends_with('\n') {
1393 result.pop();
1394 }
1395
1396 Ok(result)
1397 }
1398
1399 fn category(&self) -> RuleCategory {
1401 RuleCategory::CodeBlock
1402 }
1403
1404 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
1406 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
1409 }
1410
1411 fn as_any(&self) -> &dyn std::any::Any {
1412 self
1413 }
1414
1415 crate::impl_rule_config_sections!(MD046Config);
1416
1417 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1418 where
1419 Self: Sized,
1420 {
1421 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
1422 let style_explicit = option_is_explicit(config, "MD046", "style");
1423
1424 Box::new(Self {
1425 config: rule_config,
1426 style_explicit,
1427 })
1428 }
1429}
1430
1431#[cfg(test)]
1432mod tests {
1433 use super::*;
1434 use crate::lint_context::LintContext;
1435
1436 fn detect_style_from_content(rule: &MD046CodeBlockStyle, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
1448 let flavor = if is_mkdocs {
1449 crate::config::MarkdownFlavor::MkDocs
1450 } else {
1451 crate::config::MarkdownFlavor::Standard
1452 };
1453 let ctx = LintContext::new(content, flavor, None);
1454 let lines: Vec<&str> = content.lines().collect();
1455 let in_list_context = rule.precompute_block_continuation_context(&lines);
1456 let in_tab_context = if is_mkdocs {
1457 rule.precompute_mkdocs_tab_context(&lines)
1458 } else {
1459 vec![false; lines.len()]
1460 };
1461 let in_admonition_context = if is_mkdocs {
1462 rule.precompute_mkdocs_admonition_context(&lines)
1463 } else {
1464 vec![false; lines.len()]
1465 };
1466 let in_comment_or_html = vec![false; lines.len()];
1467 let list_item_baseline: Vec<Option<usize>> = vec![None; lines.len()];
1473 let ictx = IndentContext {
1474 in_list_context: &in_list_context,
1475 in_tab_context: &in_tab_context,
1476 in_admonition_context: &in_admonition_context,
1477 in_comment_or_html: &in_comment_or_html,
1478 list_item_baseline: &list_item_baseline,
1479 };
1480 rule.detect_style(&ctx, &lines, is_mkdocs, &ictx)
1481 }
1482
1483 #[test]
1484 fn test_fenced_code_block_detection() {
1485 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1486 assert!(rule.is_fenced_code_block_start("```"));
1487 assert!(rule.is_fenced_code_block_start("```rust"));
1488 assert!(rule.is_fenced_code_block_start("~~~"));
1489 assert!(rule.is_fenced_code_block_start("~~~python"));
1490 assert!(rule.is_fenced_code_block_start(" ```"));
1491 assert!(!rule.is_fenced_code_block_start("``"));
1492 assert!(!rule.is_fenced_code_block_start("~~"));
1493 assert!(!rule.is_fenced_code_block_start("Regular text"));
1494 }
1495
1496 #[test]
1497 fn test_consistent_style_with_fenced_blocks() {
1498 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1499 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
1500 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1501 let result = rule.check(&ctx).unwrap();
1502
1503 assert_eq!(result.len(), 0);
1505 }
1506
1507 #[test]
1508 fn test_consistent_style_with_indented_blocks() {
1509 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1510 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
1511 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1512 let result = rule.check(&ctx).unwrap();
1513
1514 assert_eq!(result.len(), 0);
1516 }
1517
1518 #[test]
1519 fn test_consistent_style_mixed() {
1520 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1521 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
1522 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1523 let result = rule.check(&ctx).unwrap();
1524
1525 assert!(!result.is_empty());
1527 }
1528
1529 #[test]
1530 fn test_fenced_style_with_indented_blocks() {
1531 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1532 let content = "Text\n\n indented code\n more code\n\nMore text";
1533 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1534 let result = rule.check(&ctx).unwrap();
1535
1536 assert!(!result.is_empty());
1538 assert!(result[0].message.contains("Use fenced code blocks"));
1539 }
1540
1541 #[test]
1542 fn test_fenced_style_with_tab_indented_blocks() {
1543 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1544 let content = "Text\n\n\ttab indented code\n\tmore code\n\nMore text";
1545 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1546 let result = rule.check(&ctx).unwrap();
1547
1548 assert!(!result.is_empty());
1550 assert!(result[0].message.contains("Use fenced code blocks"));
1551 }
1552
1553 #[test]
1554 fn test_fenced_style_with_mixed_whitespace_indented_blocks() {
1555 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1556 let content = "Text\n\n \tmixed indent code\n \tmore code\n\nMore text";
1558 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1559 let result = rule.check(&ctx).unwrap();
1560
1561 assert!(
1563 !result.is_empty(),
1564 "Mixed whitespace (2 spaces + tab) should be detected as indented code"
1565 );
1566 assert!(result[0].message.contains("Use fenced code blocks"));
1567 }
1568
1569 #[test]
1570 fn test_fenced_style_with_one_space_tab_indent() {
1571 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1572 let content = "Text\n\n \ttab after one space\n \tmore code\n\nMore text";
1574 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1575 let result = rule.check(&ctx).unwrap();
1576
1577 assert!(!result.is_empty(), "1 space + tab should be detected as indented code");
1578 assert!(result[0].message.contains("Use fenced code blocks"));
1579 }
1580
1581 #[test]
1582 fn test_indented_style_with_fenced_blocks() {
1583 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1584 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
1585 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1586 let result = rule.check(&ctx).unwrap();
1587
1588 assert!(!result.is_empty());
1590 assert!(result[0].message.contains("Use indented code blocks"));
1591 }
1592
1593 #[test]
1594 fn test_unclosed_code_block() {
1595 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1596 let content = "```\ncode without closing fence";
1597 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1598 let result = rule.check(&ctx).unwrap();
1599
1600 assert_eq!(result.len(), 1);
1601 assert!(result[0].message.contains("never closed"));
1602 }
1603
1604 #[test]
1605 fn test_nested_code_blocks() {
1606 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1607 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
1608 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1609 let result = rule.check(&ctx).unwrap();
1610
1611 assert_eq!(result.len(), 0);
1613 }
1614
1615 #[test]
1616 fn test_fix_indented_to_fenced() {
1617 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1618 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
1619 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1620 let fixed = rule.fix(&ctx).unwrap();
1621
1622 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
1623 }
1624
1625 #[test]
1626 fn test_fix_fenced_to_indented() {
1627 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1628 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
1629 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1630 let fixed = rule.fix(&ctx).unwrap();
1631
1632 assert!(fixed.contains(" code line 1\n code line 2"));
1633 assert!(!fixed.contains("```"));
1634 }
1635
1636 #[test]
1637 fn test_fix_fenced_to_indented_blank_lines_have_no_trailing_spaces() {
1638 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1642 let content = "Text\n\n```\ncode line 1\n\ncode line 2\n```\n\nMore text";
1643 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1644 let fixed = rule.fix(&ctx).unwrap();
1645
1646 for line in fixed.lines() {
1647 assert!(
1648 line.is_empty() || !line.trim_end().is_empty() || line == line.trim_end(),
1649 "no line may have trailing whitespace, got {line:?}"
1650 );
1651 assert_ne!(line, " ", "blank line was indented to trailing spaces");
1652 }
1653 assert!(fixed.contains(" code line 1\n\n code line 2"));
1655 }
1656
1657 #[test]
1658 fn test_is_list_item_requires_delimiter_after_digits() {
1659 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1660 assert!(rule.is_list_item("1. First"));
1662 assert!(rule.is_list_item("42) Item"));
1663 assert!(rule.is_list_item(" 3. Indented item"));
1664 assert!(rule.is_list_item("- bullet"));
1666 assert!(rule.is_list_item("* bullet"));
1667 assert!(!rule.is_list_item("2 results. More info."));
1670 assert!(!rule.is_list_item("3 options (a, b) here"));
1671 assert!(!rule.is_list_item("100 items in stock. Buy now"));
1672 }
1673
1674 #[test]
1675 fn test_fix_fenced_to_indented_preserves_internal_indentation() {
1676 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1679 let content = r#"# Test
1680
1681```html
1682<!doctype html>
1683<html>
1684 <head>
1685 <title>Test</title>
1686 </head>
1687</html>
1688```
1689"#;
1690 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1691 let fixed = rule.fix(&ctx).unwrap();
1692
1693 assert!(
1696 fixed.contains(" <head>"),
1697 "Expected 6 spaces before <head> (4 for code block + 2 original), got:\n{fixed}"
1698 );
1699 assert!(
1700 fixed.contains(" <title>"),
1701 "Expected 8 spaces before <title> (4 for code block + 4 original), got:\n{fixed}"
1702 );
1703 assert!(!fixed.contains("```"), "Fenced markers should be removed");
1704 }
1705
1706 #[test]
1707 fn test_fix_fenced_to_indented_preserves_python_indentation() {
1708 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1710 let content = r#"# Python Example
1711
1712```python
1713def greet(name):
1714 if name:
1715 print(f"Hello, {name}!")
1716 else:
1717 print("Hello, World!")
1718```
1719"#;
1720 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1721 let fixed = rule.fix(&ctx).unwrap();
1722
1723 assert!(
1725 fixed.contains(" def greet(name):"),
1726 "Function def should have 4 spaces (code block indent)"
1727 );
1728 assert!(
1729 fixed.contains(" if name:"),
1730 "if statement should have 8 spaces (4 code + 4 Python)"
1731 );
1732 assert!(
1733 fixed.contains(" print"),
1734 "print should have 12 spaces (4 code + 8 Python)"
1735 );
1736 }
1737
1738 #[test]
1739 fn test_fix_fenced_to_indented_preserves_yaml_indentation() {
1740 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1742 let content = r#"# Config
1743
1744```yaml
1745server:
1746 host: localhost
1747 port: 8080
1748 ssl:
1749 enabled: true
1750 cert: /path/to/cert
1751```
1752"#;
1753 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1754 let fixed = rule.fix(&ctx).unwrap();
1755
1756 assert!(fixed.contains(" server:"), "Root key should have 4 spaces");
1757 assert!(fixed.contains(" host:"), "First level should have 6 spaces");
1758 assert!(fixed.contains(" ssl:"), "ssl key should have 6 spaces");
1759 assert!(fixed.contains(" enabled:"), "Nested ssl should have 8 spaces");
1760 }
1761
1762 #[test]
1763 fn test_fix_fenced_to_indented_preserves_empty_lines() {
1764 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1768 let content = "```\nline1\n\nline2\n```\n";
1769 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1770 let fixed = rule.fix(&ctx).unwrap();
1771
1772 assert!(fixed.contains(" line1"), "line1 should be indented");
1774 assert!(fixed.contains(" line2"), "line2 should be indented");
1775 assert!(
1776 fixed.contains(" line1\n\n line2"),
1777 "blank line must stay empty, got {fixed:?}"
1778 );
1779 }
1780
1781 #[test]
1782 fn test_fix_fenced_to_indented_multiple_blocks() {
1783 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1785 let content = r#"# Doc
1786
1787```python
1788def foo():
1789 pass
1790```
1791
1792Text between.
1793
1794```yaml
1795key:
1796 value: 1
1797```
1798"#;
1799 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1800 let fixed = rule.fix(&ctx).unwrap();
1801
1802 assert!(fixed.contains(" def foo():"), "Python def should be indented");
1803 assert!(fixed.contains(" pass"), "Python body should have 8 spaces");
1804 assert!(fixed.contains(" key:"), "YAML root should have 4 spaces");
1805 assert!(fixed.contains(" value:"), "YAML nested should have 6 spaces");
1806 assert!(!fixed.contains("```"), "No fence markers should remain");
1807 }
1808
1809 #[test]
1810 fn test_fix_unclosed_block() {
1811 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1812 let content = "```\ncode without closing";
1813 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1814 let fixed = rule.fix(&ctx).unwrap();
1815
1816 assert!(fixed.ends_with("```"));
1818 }
1819
1820 #[test]
1821 fn test_code_block_in_list() {
1822 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1823 let content = "- List item\n code in list\n more code\n- Next item";
1824 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1825 let result = rule.check(&ctx).unwrap();
1826
1827 assert_eq!(result.len(), 0);
1829 }
1830
1831 #[test]
1832 fn test_detect_style_fenced() {
1833 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1834 let content = "```\ncode\n```";
1835 let style = detect_style_from_content(&rule, content, false);
1836
1837 assert_eq!(style, Some(CodeBlockStyle::Fenced));
1838 }
1839
1840 #[test]
1841 fn test_detect_style_indented() {
1842 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1843 let content = "Text\n\n code\n\nMore";
1844 let style = detect_style_from_content(&rule, content, false);
1845
1846 assert_eq!(style, Some(CodeBlockStyle::Indented));
1847 }
1848
1849 #[test]
1850 fn test_detect_style_none() {
1851 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1852 let content = "No code blocks here";
1853 let style = detect_style_from_content(&rule, content, false);
1854
1855 assert_eq!(style, None);
1856 }
1857
1858 #[test]
1859 fn test_tilde_fence() {
1860 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1861 let content = "~~~\ncode\n~~~";
1862 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1863 let result = rule.check(&ctx).unwrap();
1864
1865 assert_eq!(result.len(), 0);
1867 }
1868
1869 #[test]
1870 fn test_language_specification() {
1871 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1872 let content = "```rust\nfn main() {}\n```";
1873 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1874 let result = rule.check(&ctx).unwrap();
1875
1876 assert_eq!(result.len(), 0);
1877 }
1878
1879 #[test]
1880 fn test_empty_content() {
1881 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1882 let content = "";
1883 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1884 let result = rule.check(&ctx).unwrap();
1885
1886 assert_eq!(result.len(), 0);
1887 }
1888
1889 #[test]
1890 fn test_default_config() {
1891 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1892 let (name, _config) = rule.default_config_section().unwrap();
1893 assert_eq!(name, "MD046");
1894 }
1895
1896 #[test]
1897 fn test_markdown_documentation_block() {
1898 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1899 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
1900 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1901 let result = rule.check(&ctx).unwrap();
1902
1903 assert_eq!(result.len(), 0);
1905 }
1906
1907 #[test]
1908 fn test_preserve_trailing_newline() {
1909 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1910 let content = "```\ncode\n```\n";
1911 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1912 let fixed = rule.fix(&ctx).unwrap();
1913
1914 assert_eq!(fixed, content);
1915 }
1916
1917 #[test]
1918 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
1919 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1920 let content = r#"# Document
1921
1922=== "Python"
1923
1924 This is tab content
1925 Not an indented code block
1926
1927 ```python
1928 def hello():
1929 print("Hello")
1930 ```
1931
1932=== "JavaScript"
1933
1934 More tab content here
1935 Also not an indented code block"#;
1936
1937 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1938 let result = rule.check(&ctx).unwrap();
1939
1940 assert_eq!(result.len(), 0);
1942 }
1943
1944 #[test]
1945 fn test_mkdocs_tabs_with_actual_indented_code() {
1946 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1947 let content = r#"# Document
1948
1949=== "Tab 1"
1950
1951 This is tab content
1952
1953Regular text
1954
1955 This is an actual indented code block
1956 Should be flagged"#;
1957
1958 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1959 let result = rule.check(&ctx).unwrap();
1960
1961 assert_eq!(result.len(), 1);
1963 assert!(result[0].message.contains("Use fenced code blocks"));
1964 }
1965
1966 #[test]
1967 fn test_mkdocs_tabs_detect_style() {
1968 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1969 let content = r#"=== "Tab 1"
1970
1971 Content in tab
1972 More content
1973
1974=== "Tab 2"
1975
1976 Content in second tab"#;
1977
1978 let style = detect_style_from_content(&rule, content, true);
1980 assert_eq!(style, None); let style = detect_style_from_content(&rule, content, false);
1984 assert_eq!(style, Some(CodeBlockStyle::Indented));
1985 }
1986
1987 #[test]
1988 fn test_mkdocs_nested_tabs() {
1989 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1990 let content = r#"# Document
1991
1992=== "Outer Tab"
1993
1994 Some content
1995
1996 === "Nested Tab"
1997
1998 Nested tab content
1999 Should not be flagged"#;
2000
2001 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2002 let result = rule.check(&ctx).unwrap();
2003
2004 assert_eq!(result.len(), 0);
2006 }
2007
2008 #[test]
2009 fn test_mkdocs_admonitions_not_flagged_as_indented_code() {
2010 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2013 let content = r#"# Document
2014
2015!!! note
2016 This is normal admonition content, not a code block.
2017 It spans multiple lines.
2018
2019??? warning "Collapsible Warning"
2020 This is also admonition content.
2021
2022???+ tip "Expanded Tip"
2023 And this one too.
2024
2025Regular text outside admonitions."#;
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 "Admonition content in MkDocs mode should not trigger MD046"
2035 );
2036 }
2037
2038 #[test]
2039 fn test_mkdocs_admonition_with_actual_indented_code() {
2040 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2042 let content = r#"# Document
2043
2044!!! note
2045 This is admonition content.
2046
2047Regular text ends the admonition.
2048
2049 This is actual indented code (should be flagged)"#;
2050
2051 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2052 let result = rule.check(&ctx).unwrap();
2053
2054 assert_eq!(result.len(), 1);
2056 assert!(result[0].message.contains("Use fenced code blocks"));
2057 }
2058
2059 #[test]
2060 fn test_admonition_in_standard_mode_flagged() {
2061 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2065 let content = r#"# Document
2066
2067!!! note
2068
2069 This looks like code in standard mode.
2070
2071Regular text."#;
2072
2073 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2075 let result = rule.check(&ctx).unwrap();
2076
2077 assert_eq!(
2079 result.len(),
2080 1,
2081 "Admonition content in Standard mode should be flagged as indented code"
2082 );
2083 }
2084
2085 #[test]
2086 fn test_mkdocs_admonition_with_fenced_code_inside() {
2087 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2089 let content = r#"# Document
2090
2091!!! note "Code Example"
2092 Here's some code:
2093
2094 ```python
2095 def hello():
2096 print("world")
2097 ```
2098
2099 More text after code.
2100
2101Regular text."#;
2102
2103 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2104 let result = rule.check(&ctx).unwrap();
2105
2106 assert_eq!(result.len(), 0, "Fenced code blocks inside admonitions should be valid");
2108 }
2109
2110 #[test]
2111 fn test_mkdocs_nested_admonitions() {
2112 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2114 let content = r#"# Document
2115
2116!!! note "Outer"
2117 Outer content.
2118
2119 !!! warning "Inner"
2120 Inner content.
2121 More inner content.
2122
2123 Back to outer.
2124
2125Regular text."#;
2126
2127 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2128 let result = rule.check(&ctx).unwrap();
2129
2130 assert_eq!(result.len(), 0, "Nested admonitions should not be flagged");
2132 }
2133
2134 #[test]
2135 fn test_mkdocs_admonition_fix_does_not_wrap() {
2136 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2138 let content = r#"!!! note
2139 Content that should stay as admonition content.
2140 Not be wrapped in code fences.
2141"#;
2142
2143 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2144 let fixed = rule.fix(&ctx).unwrap();
2145
2146 assert!(
2148 !fixed.contains("```\n Content"),
2149 "Admonition content should not be wrapped in fences"
2150 );
2151 assert_eq!(fixed, content, "Content should remain unchanged");
2152 }
2153
2154 #[test]
2155 fn test_mkdocs_empty_admonition() {
2156 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2158 let content = r#"!!! note
2159
2160Regular paragraph after empty admonition.
2161
2162 This IS an indented code block (after blank + non-indented line)."#;
2163
2164 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2165 let result = rule.check(&ctx).unwrap();
2166
2167 assert_eq!(result.len(), 1, "Indented code after admonition ends should be flagged");
2169 }
2170
2171 #[test]
2172 fn test_mkdocs_indented_admonition() {
2173 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2175 let content = r#"- List item
2176
2177 !!! note
2178 Indented admonition content.
2179 More content.
2180
2181- Next item"#;
2182
2183 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2184 let result = rule.check(&ctx).unwrap();
2185
2186 assert_eq!(
2188 result.len(),
2189 0,
2190 "Indented admonitions (e.g., in lists) should not be flagged"
2191 );
2192 }
2193
2194 #[test]
2195 fn test_footnote_indented_paragraphs_not_flagged() {
2196 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2197 let content = r#"# Test Document with Footnotes
2198
2199This is some text with a footnote[^1].
2200
2201Here's some code:
2202
2203```bash
2204echo "fenced code block"
2205```
2206
2207More text with another footnote[^2].
2208
2209[^1]: Really interesting footnote text.
2210
2211 Even more interesting second paragraph.
2212
2213[^2]: Another footnote.
2214
2215 With a second paragraph too.
2216
2217 And even a third paragraph!"#;
2218
2219 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2220 let result = rule.check(&ctx).unwrap();
2221
2222 assert_eq!(result.len(), 0);
2224 }
2225
2226 #[test]
2227 fn test_footnote_definition_detection() {
2228 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2229
2230 assert!(rule.is_footnote_definition("[^1]: Footnote text"));
2233 assert!(rule.is_footnote_definition("[^foo]: Footnote text"));
2234 assert!(rule.is_footnote_definition("[^long-name]: Footnote text"));
2235 assert!(rule.is_footnote_definition("[^test_123]: Mixed chars"));
2236 assert!(rule.is_footnote_definition(" [^1]: Indented footnote"));
2237 assert!(rule.is_footnote_definition("[^a]: Minimal valid footnote"));
2238 assert!(rule.is_footnote_definition("[^123]: Numeric label"));
2239 assert!(rule.is_footnote_definition("[^_]: Single underscore"));
2240 assert!(rule.is_footnote_definition("[^-]: Single hyphen"));
2241
2242 assert!(!rule.is_footnote_definition("[^]: No label"));
2244 assert!(!rule.is_footnote_definition("[^ ]: Whitespace only"));
2245 assert!(!rule.is_footnote_definition("[^ ]: Multiple spaces"));
2246 assert!(!rule.is_footnote_definition("[^\t]: Tab only"));
2247
2248 assert!(!rule.is_footnote_definition("[^]]: Extra bracket"));
2250 assert!(!rule.is_footnote_definition("Regular text [^1]:"));
2251 assert!(!rule.is_footnote_definition("[1]: Not a footnote"));
2252 assert!(!rule.is_footnote_definition("[^")); assert!(!rule.is_footnote_definition("[^1:")); assert!(!rule.is_footnote_definition("^1]: Missing opening bracket"));
2255
2256 assert!(!rule.is_footnote_definition("[^test.name]: Period"));
2258 assert!(!rule.is_footnote_definition("[^test name]: Space in label"));
2259 assert!(!rule.is_footnote_definition("[^test@name]: Special char"));
2260 assert!(!rule.is_footnote_definition("[^test/name]: Slash"));
2261 assert!(!rule.is_footnote_definition("[^test\\name]: Backslash"));
2262
2263 assert!(!rule.is_footnote_definition("[^test\r]: Carriage return"));
2266 }
2267
2268 #[test]
2269 fn test_footnote_with_blank_lines() {
2270 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2274 let content = r#"# Document
2275
2276Text with footnote[^1].
2277
2278[^1]: First paragraph.
2279
2280 Second paragraph after blank line.
2281
2282 Third paragraph after another blank line.
2283
2284Regular text at column 0 ends the footnote."#;
2285
2286 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2287 let result = rule.check(&ctx).unwrap();
2288
2289 assert_eq!(
2291 result.len(),
2292 0,
2293 "Indented content within footnotes should not trigger MD046"
2294 );
2295 }
2296
2297 #[test]
2298 fn test_footnote_multiple_consecutive_blank_lines() {
2299 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2302 let content = r#"Text[^1].
2303
2304[^1]: First paragraph.
2305
2306
2307
2308 Content after three blank lines (still part of footnote).
2309
2310Not indented, so footnote ends here."#;
2311
2312 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2313 let result = rule.check(&ctx).unwrap();
2314
2315 assert_eq!(
2317 result.len(),
2318 0,
2319 "Multiple blank lines shouldn't break footnote continuation"
2320 );
2321 }
2322
2323 #[test]
2324 fn test_footnote_terminated_by_non_indented_content() {
2325 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2328 let content = r#"[^1]: Footnote content.
2329
2330 More indented content in footnote.
2331
2332This paragraph is not indented, so footnote ends.
2333
2334 This should be flagged as indented 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 "Indented code after footnote termination should be flagged"
2344 );
2345 assert!(
2346 result[0].message.contains("Use fenced code blocks"),
2347 "Expected MD046 warning for indented code block"
2348 );
2349 assert!(result[0].line >= 7, "Warning should be on the indented code block line");
2350 }
2351
2352 #[test]
2353 fn test_footnote_terminated_by_structural_elements() {
2354 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2356 let content = r#"[^1]: Footnote content.
2357
2358 More content.
2359
2360## Heading terminates footnote
2361
2362 This indented content should be flagged.
2363
2364---
2365
2366 This should also be flagged (after horizontal rule)."#;
2367
2368 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2369 let result = rule.check(&ctx).unwrap();
2370
2371 assert_eq!(
2373 result.len(),
2374 2,
2375 "Both indented blocks after termination should be flagged"
2376 );
2377 }
2378
2379 #[test]
2380 fn test_footnote_with_code_block_inside() {
2381 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2384 let content = r#"Text[^1].
2385
2386[^1]: Footnote with code:
2387
2388 ```python
2389 def hello():
2390 print("world")
2391 ```
2392
2393 More footnote text after code."#;
2394
2395 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2396 let result = rule.check(&ctx).unwrap();
2397
2398 assert_eq!(result.len(), 0, "Fenced code blocks within footnotes should be allowed");
2400 }
2401
2402 #[test]
2403 fn test_footnote_with_8_space_indented_code() {
2404 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2407 let content = r#"Text[^1].
2408
2409[^1]: Footnote with nested code.
2410
2411 code block
2412 more code"#;
2413
2414 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2415 let result = rule.check(&ctx).unwrap();
2416
2417 assert_eq!(
2419 result.len(),
2420 0,
2421 "8-space indented code within footnotes represents nested code blocks"
2422 );
2423 }
2424
2425 #[test]
2426 fn test_multiple_footnotes() {
2427 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2430 let content = r#"Text[^1] and more[^2].
2431
2432[^1]: First footnote.
2433
2434 Continuation of first.
2435
2436[^2]: Second footnote starts here, ending the first.
2437
2438 Continuation of second."#;
2439
2440 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2441 let result = rule.check(&ctx).unwrap();
2442
2443 assert_eq!(
2445 result.len(),
2446 0,
2447 "Multiple footnotes should each maintain their continuation context"
2448 );
2449 }
2450
2451 #[test]
2452 fn test_list_item_ends_footnote_context() {
2453 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2455 let content = r#"[^1]: Footnote.
2456
2457 Content in footnote.
2458
2459- List item starts here (ends footnote context).
2460
2461 This indented content is part of the list, not the footnote."#;
2462
2463 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2464 let result = rule.check(&ctx).unwrap();
2465
2466 assert_eq!(
2468 result.len(),
2469 0,
2470 "List items should end footnote context and start their own"
2471 );
2472 }
2473
2474 #[test]
2475 fn test_footnote_vs_actual_indented_code() {
2476 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2479 let content = r#"# Heading
2480
2481Text with footnote[^1].
2482
2483[^1]: Footnote content.
2484
2485 Part of footnote (should not be flagged).
2486
2487Regular paragraph ends footnote context.
2488
2489 This is actual indented code (MUST be flagged)
2490 Should be detected as code block"#;
2491
2492 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2493 let result = rule.check(&ctx).unwrap();
2494
2495 assert_eq!(
2497 result.len(),
2498 1,
2499 "Must still detect indented code blocks outside footnotes"
2500 );
2501 assert!(
2502 result[0].message.contains("Use fenced code blocks"),
2503 "Expected MD046 warning for indented code"
2504 );
2505 assert!(
2506 result[0].line >= 11,
2507 "Warning should be on the actual indented code line"
2508 );
2509 }
2510
2511 #[test]
2512 fn test_spec_compliant_label_characters() {
2513 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2516
2517 assert!(rule.is_footnote_definition("[^test]: text"));
2519 assert!(rule.is_footnote_definition("[^TEST]: text"));
2520 assert!(rule.is_footnote_definition("[^test-name]: text"));
2521 assert!(rule.is_footnote_definition("[^test_name]: text"));
2522 assert!(rule.is_footnote_definition("[^test123]: text"));
2523 assert!(rule.is_footnote_definition("[^123]: text"));
2524 assert!(rule.is_footnote_definition("[^a1b2c3]: text"));
2525
2526 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")); }
2534
2535 #[test]
2536 fn test_code_block_inside_html_comment() {
2537 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2540 let content = r#"# Document
2541
2542Some text.
2543
2544<!--
2545Example code block in comment:
2546
2547```typescript
2548console.log("Hello");
2549```
2550
2551More comment text.
2552-->
2553
2554More content."#;
2555
2556 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2557 let result = rule.check(&ctx).unwrap();
2558
2559 assert_eq!(
2560 result.len(),
2561 0,
2562 "Code blocks inside HTML comments should not be flagged as unclosed"
2563 );
2564 }
2565
2566 #[test]
2567 fn test_unclosed_fence_inside_html_comment() {
2568 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2570 let content = r#"# Document
2571
2572<!--
2573Example with intentionally unclosed fence:
2574
2575```
2576code without closing
2577-->
2578
2579More content."#;
2580
2581 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2582 let result = rule.check(&ctx).unwrap();
2583
2584 assert_eq!(
2585 result.len(),
2586 0,
2587 "Unclosed fences inside HTML comments should be ignored"
2588 );
2589 }
2590
2591 #[test]
2592 fn test_multiline_html_comment_with_indented_code() {
2593 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2595 let content = r#"# Document
2596
2597<!--
2598Example:
2599
2600 indented code
2601 more code
2602
2603End of comment.
2604-->
2605
2606Regular text."#;
2607
2608 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2609 let result = rule.check(&ctx).unwrap();
2610
2611 assert_eq!(
2612 result.len(),
2613 0,
2614 "Indented code inside HTML comments should not be flagged"
2615 );
2616 }
2617
2618 #[test]
2619 fn test_code_block_after_html_comment() {
2620 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2622 let content = r#"# Document
2623
2624<!-- comment -->
2625
2626Text before.
2627
2628 indented code should be flagged
2629
2630More text."#;
2631
2632 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2633 let result = rule.check(&ctx).unwrap();
2634
2635 assert_eq!(
2636 result.len(),
2637 1,
2638 "Code blocks after HTML comments should still be detected"
2639 );
2640 assert!(result[0].message.contains("Use fenced code blocks"));
2641 }
2642
2643 #[test]
2644 fn test_consistent_style_indented_html_comment() {
2645 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2651 let content = "# MD046 false-positive reproduction\n\
2652 \n\
2653 <!--\n \
2654 This is just an indented comment, not a code block.\n\
2655 \n \
2656 A second line is required to trigger the false-positive.\n\
2657 \n \
2658 Actually, three lines are required.\n\
2659 -->\n\
2660 \n\
2661 ```md\n\
2662 This should be fine, since it's the only code block and therefore consistent.\n\
2663 ```\n";
2664
2665 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2666 let result = rule.check(&ctx).unwrap();
2667
2668 assert_eq!(
2669 result,
2670 vec![],
2671 "A single fenced block and an indented HTML comment must produce no MD046 warnings",
2672 );
2673 }
2674
2675 #[test]
2676 fn test_consistent_style_indented_html_block() {
2677 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2684 let content = "# Heading\n\
2685 \n\
2686 <div class=\"note\">\n \
2687 line one of indented html content\n \
2688 line two of indented html content\n \
2689 line three of indented html content\n\
2690 </div>\n\
2691 \n\
2692 ```md\n\
2693 real fenced block\n\
2694 ```\n";
2695
2696 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2697 let result = rule.check(&ctx).unwrap();
2698
2699 assert_eq!(
2700 result,
2701 vec![],
2702 "Indented content inside a raw HTML block must not influence MD046 style detection",
2703 );
2704 }
2705
2706 #[test]
2707 fn test_consistent_style_fake_fence_inside_html_comment() {
2708 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2714 let content = "# Title\n\
2715 \n\
2716 <!--\n\
2717 ```\n\
2718 fake fence inside comment\n\
2719 ```\n\
2720 -->\n\
2721 \n \
2722 real indented code block line 1\n \
2723 real indented code block line 2\n";
2724
2725 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2726 let result = rule.check(&ctx).unwrap();
2727
2728 assert_eq!(
2729 result,
2730 vec![],
2731 "Fence markers inside an HTML comment must not influence MD046 style detection",
2732 );
2733 }
2734
2735 #[test]
2736 fn test_consistent_style_indented_footnote_definition() {
2737 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2741 let content = "# Heading\n\
2742 \n\
2743 Reference to a footnote[^note].\n\
2744 \n\
2745 [^note]: First line of the footnote.\n \
2746 Second indented continuation line.\n \
2747 Third indented continuation line.\n \
2748 Fourth indented continuation line.\n\
2749 \n\
2750 ```md\n\
2751 real fenced block\n\
2752 ```\n";
2753
2754 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2755 let result = rule.check(&ctx).unwrap();
2756
2757 assert_eq!(
2758 result,
2759 vec![],
2760 "Footnote-definition continuation content must not influence MD046 style detection",
2761 );
2762 }
2763
2764 #[test]
2765 fn test_consistent_style_indented_blockquote() {
2766 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2771 let content = "# Heading\n\
2772 \n\
2773 > line one of quoted indented content\n\
2774 >\n\
2775 > line two of quoted indented content\n\
2776 >\n\
2777 > line three of quoted indented content\n\
2778 \n\
2779 ```md\n\
2780 real fenced block\n\
2781 ```\n";
2782
2783 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2784 let result = rule.check(&ctx).unwrap();
2785
2786 assert_eq!(
2787 result,
2788 vec![],
2789 "Indented content inside a blockquote must not influence MD046 style detection",
2790 );
2791 }
2792
2793 #[test]
2794 fn test_consistent_style_genuine_indented_block_detected_as_indented() {
2795 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2800 let content = "# Heading\n\
2801 \n\
2802 Some prose.\n\
2803 \n \
2804 real indented code line 1\n \
2805 real indented code line 2\n";
2806
2807 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2808 let result = rule.check(&ctx).unwrap();
2809
2810 assert_eq!(
2813 result,
2814 vec![],
2815 "A genuine top-level indented block must be detected as Indented style under Consistent",
2816 );
2817 }
2818
2819 #[test]
2820 fn test_consistent_style_skipped_lines_dont_override_real_block() {
2821 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2826 let content = "# Heading\n\
2827 \n\
2828 <!--\n \
2829 skipped indented comment line 1\n \
2830 skipped indented comment line 2\n\
2831 -->\n\
2832 \n\
2833 <!--\n \
2834 second skipped region\n \
2835 also skipped\n\
2836 -->\n\
2837 \n \
2838 real indented code line\n";
2839
2840 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2841 let result = rule.check(&ctx).unwrap();
2842
2843 assert_eq!(
2844 result,
2845 vec![],
2846 "Skipped container lines must not outweigh the single real indented block",
2847 );
2848 }
2849
2850 #[test]
2851 fn test_consistent_style_fenced_wins_over_skipped_indented() {
2852 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2856 let content = "# Heading\n\
2857 \n\
2858 <!--\n \
2859 skipped indented region one\n \
2860 more of region one\n\
2861 -->\n\
2862 \n\
2863 <!--\n \
2864 skipped indented region two\n \
2865 more of region two\n\
2866 -->\n\
2867 \n\
2868 ```md\n\
2869 real fenced block\n\
2870 ```\n";
2871
2872 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2873 let result = rule.check(&ctx).unwrap();
2874
2875 assert_eq!(
2876 result,
2877 vec![],
2878 "Fenced block must win when all indented lines are inside skipped containers",
2879 );
2880 }
2881
2882 #[test]
2883 fn test_four_space_indented_fence_is_not_valid_fence() {
2884 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2887
2888 assert!(rule.is_fenced_code_block_start("```"));
2890 assert!(rule.is_fenced_code_block_start(" ```"));
2891 assert!(rule.is_fenced_code_block_start(" ```"));
2892 assert!(rule.is_fenced_code_block_start(" ```"));
2893
2894 assert!(!rule.is_fenced_code_block_start(" ```"));
2896 assert!(!rule.is_fenced_code_block_start(" ```"));
2897 assert!(!rule.is_fenced_code_block_start(" ```"));
2898
2899 assert!(!rule.is_fenced_code_block_start("\t```"));
2901 }
2902
2903 #[test]
2904 fn test_issue_237_indented_fenced_block_detected_as_indented() {
2905 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2911
2912 let content = r#"## Test
2914
2915 ```js
2916 var foo = "hello";
2917 ```
2918"#;
2919
2920 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2921 let result = rule.check(&ctx).unwrap();
2922
2923 assert_eq!(
2925 result.len(),
2926 1,
2927 "4-space indented fence should be detected as indented code block"
2928 );
2929 assert!(
2930 result[0].message.contains("Use fenced code blocks"),
2931 "Expected 'Use fenced code blocks' message"
2932 );
2933 }
2934
2935 #[test]
2936 fn test_issue_276_indented_code_in_list() {
2937 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2940
2941 let content = r#"1. First item
29422. Second item with code:
2943
2944 # This is a code block in a list
2945 print("Hello, world!")
2946
29474. Third item"#;
2948
2949 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2950 let result = rule.check(&ctx).unwrap();
2951
2952 assert!(
2954 !result.is_empty(),
2955 "Indented code block inside list should be flagged when style=fenced"
2956 );
2957 assert!(
2958 result[0].message.contains("Use fenced code blocks"),
2959 "Expected 'Use fenced code blocks' message"
2960 );
2961 }
2962
2963 #[test]
2964 fn test_three_space_indented_fence_is_valid() {
2965 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2967
2968 let content = r#"## Test
2969
2970 ```js
2971 var foo = "hello";
2972 ```
2973"#;
2974
2975 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2976 let result = rule.check(&ctx).unwrap();
2977
2978 assert_eq!(
2980 result.len(),
2981 0,
2982 "3-space indented fence should be recognized as valid fenced code block"
2983 );
2984 }
2985
2986 #[test]
2987 fn test_indented_style_with_deeply_indented_fenced() {
2988 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2991
2992 let content = r#"Text
2993
2994 ```js
2995 var foo = "hello";
2996 ```
2997
2998More text
2999"#;
3000
3001 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3002 let result = rule.check(&ctx).unwrap();
3003
3004 assert_eq!(
3007 result.len(),
3008 0,
3009 "4-space indented content should be valid when style=indented"
3010 );
3011 }
3012
3013 #[test]
3014 fn test_fix_misplaced_fenced_block() {
3015 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3018
3019 let content = r#"## Test
3020
3021 ```js
3022 var foo = "hello";
3023 ```
3024"#;
3025
3026 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3027 let fixed = rule.fix(&ctx).unwrap();
3028
3029 let expected = r#"## Test
3031
3032```js
3033var foo = "hello";
3034```
3035"#;
3036
3037 assert_eq!(fixed, expected, "Fix should remove indentation, not add more fences");
3038 }
3039
3040 #[test]
3041 fn test_fix_regular_indented_block() {
3042 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3045
3046 let content = r#"Text
3047
3048 var foo = "hello";
3049 console.log(foo);
3050
3051More text
3052"#;
3053
3054 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3055 let fixed = rule.fix(&ctx).unwrap();
3056
3057 assert!(fixed.contains("```\nvar foo"), "Should add opening fence");
3059 assert!(fixed.contains("console.log(foo);\n```"), "Should add closing fence");
3060 }
3061
3062 #[test]
3063 fn test_fix_indented_block_with_fence_like_content() {
3064 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3068
3069 let content = r#"Text
3070
3071 some code
3072 ```not a fence opener
3073 more code
3074"#;
3075
3076 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3077 let fixed = rule.fix(&ctx).unwrap();
3078
3079 assert!(fixed.contains(" some code"), "Unsafe block should be left unchanged");
3081 assert!(!fixed.contains("```\nsome code"), "Should NOT wrap unsafe block");
3082 }
3083
3084 #[test]
3085 fn test_fix_mixed_indented_and_misplaced_blocks() {
3086 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3088
3089 let content = r#"Text
3090
3091 regular indented code
3092
3093More text
3094
3095 ```python
3096 print("hello")
3097 ```
3098"#;
3099
3100 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3101 let fixed = rule.fix(&ctx).unwrap();
3102
3103 assert!(
3105 fixed.contains("```\nregular indented code\n```"),
3106 "First block should be wrapped in fences"
3107 );
3108
3109 assert!(
3111 fixed.contains("\n```python\nprint(\"hello\")\n```"),
3112 "Second block should be dedented, not double-wrapped"
3113 );
3114 assert!(
3116 !fixed.contains("```\n```python"),
3117 "Should not have nested fence openers"
3118 );
3119 }
3120
3121 #[test]
3122 fn test_md046_front_matter() {
3123 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3124 let content = "---\nmetadata:\n\n description: Indented\n---\n";
3125 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3126 let result = rule.check(&ctx).unwrap();
3127 assert_eq!(result.len(), 0);
3128 }
3129
3130 #[test]
3131 fn test_md046_fix_front_matter() {
3132 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3133 let content = "---\nmetadata:\n\n description: Indented\n---\n";
3134 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3135 let fixed = rule.fix(&ctx).unwrap();
3136 assert_eq!(fixed, content);
3137 }
3138
3139 #[test]
3140 fn test_whitespace_only_line_is_not_an_indented_code_block() {
3141 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3146 let content = "# T\n\nPara\n\n \nMore\n\n real code\n\nEnd\n";
3147 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3148 let fixed = rule.fix(&ctx).unwrap();
3149 assert_eq!(fixed, "# T\n\nPara\n\n \nMore\n\n```\nreal code\n```\n\nEnd\n");
3150 }
3151
3152 #[test]
3153 fn test_interior_blank_line_keeps_indented_block_together() {
3154 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3158 let content = "# T\n\nPara\n\n a\n\n b\n\nAfter\n";
3159 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3160 let fixed = rule.fix(&ctx).unwrap();
3161 assert_eq!(fixed, "# T\n\nPara\n\n```\na\n\nb\n```\n\nAfter\n");
3162 }
3163
3164 #[test]
3165 fn test_consistent_style_counts_a_block_with_interior_blank_once() {
3166 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3170 let content = "# T\n\n```\nfenced\n```\n\nPara\n\n a\n\n b\n\nEnd\n";
3171 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3172 let result = rule.check(&ctx).unwrap();
3173 let reported: Vec<(usize, &str)> = result.iter().map(|w| (w.line, w.message.as_str())).collect();
3174 assert_eq!(reported, vec![(9, "Use fenced code blocks")]);
3175 }
3176
3177 #[test]
3178 fn test_indented_lazy_continuation_lines_are_not_code() {
3179 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3185 let content = "# T\n\nPara\n lazy one\n lazy two\n lazy three\n\n real code\n\nEnd\n";
3186 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3187 let fixed = rule.fix(&ctx).unwrap();
3188 assert_eq!(
3189 fixed,
3190 "# T\n\nPara\n lazy one\n lazy two\n lazy three\n\n```\nreal code\n```\n\nEnd\n"
3191 );
3192 }
3193
3194 #[test]
3195 fn test_misplaced_fence_with_interior_blank_dedents_as_one_block() {
3196 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3200 let content = "# T\n\nPara\n\n ```python\n x = 1\n\n y = 2\n ```\n\nAfter\n";
3201 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3202 let fixed = rule.fix(&ctx).unwrap();
3203 assert_eq!(fixed, "# T\n\nPara\n\n```python\nx = 1\n\ny = 2\n```\n\nAfter\n");
3204 }
3205 #[test]
3206 fn test_mdg_overrides_indented_style_to_fenced() {
3207 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
3212 let content = "# Feature: Payloads\n\n## Scenario: JSON payload\n\n* Given this payload\n\n ```json\n {\"ok\": true}\n ```\n";
3213
3214 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3215 assert!(rule.check(&mdg_ctx).unwrap().is_empty());
3216 assert_eq!(rule.fix(&mdg_ctx).unwrap(), content);
3217
3218 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3220 let standard_warnings = rule.check(&standard_ctx).unwrap();
3221 assert_eq!(standard_warnings.len(), 1);
3222 assert_eq!(standard_warnings[0].message, "Use indented code blocks");
3223 assert!(!rule.fix(&standard_ctx).unwrap().contains("```"));
3224 }
3225
3226 #[test]
3227 fn test_mdg_indented_style_still_fences_indented_blocks() {
3228 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
3232 let content =
3233 "# Feature: Payloads\n\n## Scenario: Plain payload\n\nSome description.\n\n ordinary indented code\n";
3234
3235 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3236 let warnings = rule.check(&mdg_ctx).unwrap();
3237 assert_eq!(warnings.len(), 1);
3238 assert_eq!(warnings[0].message, "Use fenced code blocks");
3239
3240 let fixed = rule.fix(&mdg_ctx).unwrap();
3241 assert_eq!(
3242 fixed,
3243 "# Feature: Payloads\n\n## Scenario: Plain payload\n\nSome description.\n\n```\n ordinary indented code\n```\n"
3244 );
3245
3246 let fixed_ctx = LintContext::new(&fixed, crate::config::MarkdownFlavor::MDG, None);
3247 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
3248 assert_eq!(rule.fix(&fixed_ctx).unwrap(), fixed, "MDG fix should be idempotent");
3249
3250 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3253 assert!(rule.check(&standard_ctx).unwrap().is_empty());
3254 assert_eq!(rule.fix(&standard_ctx).unwrap(), content);
3255 }
3256
3257 #[test]
3258 fn test_mdg_steers_indented_code_to_fenced() {
3259 let content = "# Feature: Payloads\n\n## Scenario: Plain payload\n\n* Given this payload\n\n ordinary indented code\n";
3263
3264 for rule in [
3265 MD046CodeBlockStyle::new(CodeBlockStyle::Fenced),
3266 MD046CodeBlockStyle::new(CodeBlockStyle::Consistent),
3267 MD046CodeBlockStyle::new(CodeBlockStyle::Indented),
3268 ] {
3269 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3270 let warnings = rule.check(&ctx).unwrap();
3271 assert_eq!(warnings.len(), 1);
3272 assert_eq!(warnings[0].message, "Use fenced code blocks");
3273
3274 let fixed = rule.fix(&ctx).unwrap();
3275 assert!(fixed.contains("```"), "MDG must fence the block: {fixed:?}");
3276
3277 let fixed_ctx = LintContext::new(&fixed, crate::config::MarkdownFlavor::MDG, None);
3278 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
3279 assert_eq!(rule.fix(&fixed_ctx).unwrap(), fixed, "MDG fix should be idempotent");
3280 }
3281 }
3282
3283 #[test]
3284 fn test_mdg_consistent_style_ignores_indented_prevalence() {
3285 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3288 let indented_majority = "# Feature: Payloads\n\n## Scenario: Mixed payloads\n\n* Given this payload\n\n```json\n{\"ok\": true}\n```\n\nFirst ordinary example:\n\n one\n\nSecond ordinary example:\n\n two\n";
3289
3290 let standard_ctx = LintContext::new(indented_majority, crate::config::MarkdownFlavor::Standard, None);
3291 let standard_warnings = rule.check(&standard_ctx).unwrap();
3292 assert_eq!(standard_warnings.len(), 1);
3293 assert_eq!(standard_warnings[0].message, "Use indented code blocks");
3294
3295 let mdg_ctx = LintContext::new(indented_majority, crate::config::MarkdownFlavor::MDG, None);
3296 let mdg_warnings = rule.check(&mdg_ctx).unwrap();
3297 assert_eq!(mdg_warnings.len(), 2);
3298 assert!(
3299 mdg_warnings
3300 .iter()
3301 .all(|warning| warning.message == "Use fenced code blocks")
3302 );
3303 }
3304
3305 #[test]
3306 fn test_mdg_repairs_unclosed_fence_like_standard() {
3307 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3310 let content = "# Feature: Payloads\n\n```json\n{\"ok\": true}\n";
3311
3312 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3313 let warnings = rule.check(&mdg_ctx).unwrap();
3314 assert_eq!(warnings.len(), 1);
3315 assert!(warnings[0].message.contains("never closed"));
3316
3317 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3318 assert_eq!(
3319 rule.fix(&mdg_ctx).unwrap(),
3320 rule.fix(&standard_ctx).unwrap(),
3321 "MDG must not differ from Standard"
3322 );
3323 }
3324
3325 #[test]
3326 fn test_mdg_table_above_prose_is_never_fenced() {
3327 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3333 let content = "# Feature: Eating\n\n#### Examples:\n\n | start | eat | left |\n | ----- | --- | ---- |\n\n a note about the data\n\n## Scenario: Other\n\n unrelated indented code\n";
3334
3335 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3336 let reported: Vec<usize> = rule.check(&mdg_ctx).unwrap().iter().map(|w| w.line).collect();
3337 assert_eq!(reported, vec![8, 12]);
3338
3339 let fixed = rule.fix(&mdg_ctx).unwrap();
3340 assert_eq!(
3341 fixed,
3342 "# Feature: Eating\n\n#### Examples:\n\n | start | eat | left |\n | ----- | --- | ---- |\n\n```\na note about the data\n```\n\n## Scenario: Other\n\n```\n unrelated indented code\n```\n"
3343 );
3344
3345 let fixed_ctx = LintContext::new(&fixed, crate::config::MarkdownFlavor::MDG, None);
3346 assert!(
3347 rule.check(&fixed_ctx).unwrap().is_empty(),
3348 "MDG check must have nothing left to report after its own fix"
3349 );
3350 assert_eq!(rule.fix(&fixed_ctx).unwrap(), fixed, "MDG fix should be idempotent");
3351
3352 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3354 let standard_reported: Vec<usize> = rule.check(&standard_ctx).unwrap().iter().map(|w| w.line).collect();
3355 assert_eq!(standard_reported, vec![5, 12]);
3356 assert!(rule.fix(&standard_ctx).unwrap().contains("```\n| start | eat | left |"));
3357 }
3358
3359 #[test]
3360 fn test_mdg_repairs_unclosed_fence_under_indented_style() {
3361 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
3365 let content = "# Feature: Payloads\n\n```json\n{\"ok\": true}\n";
3366
3367 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3368 let warnings = rule.check(&mdg_ctx).unwrap();
3369 assert_eq!(warnings.len(), 1);
3370 assert!(warnings[0].message.contains("never closed"));
3371
3372 let fixed = rule.fix(&mdg_ctx).unwrap();
3373 assert_eq!(fixed, "# Feature: Payloads\n\n```json\n{\"ok\": true}\n```\n");
3374
3375 let fixed_ctx = LintContext::new(&fixed, crate::config::MarkdownFlavor::MDG, None);
3376 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
3377
3378 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3380 assert!(rule.fix(&standard_ctx).unwrap().contains("\n {\"ok\": true}\n"));
3381 }
3382
3383 #[test]
3384 fn test_mdg_tab_indented_table_is_not_code() {
3385 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3389 for indent in ["\t\t", " \t"] {
3390 let content = format!(
3391 "# Feature: Eating\n\n#### Examples:\n\n{indent}| start | eat |\n{indent}| ----- | --- |\n\n## Scenario: Other\n\n code here\n"
3392 );
3393
3394 let mdg_ctx = LintContext::new(&content, crate::config::MarkdownFlavor::MDG, None);
3395 let reported: Vec<usize> = rule.check(&mdg_ctx).unwrap().iter().map(|w| w.line).collect();
3396 assert_eq!(reported, vec![10], "tab-indented rows are a table, not code");
3397
3398 let fixed = rule.fix(&mdg_ctx).unwrap();
3399 assert!(
3400 fixed.contains(&format!("{indent}| start | eat |\n{indent}| ----- | --- |")),
3401 "MDG must leave the tab-indented table alone: {fixed:?}"
3402 );
3403
3404 let standard_ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
3405 let standard_reported: Vec<usize> = rule.check(&standard_ctx).unwrap().iter().map(|w| w.line).collect();
3406 assert_eq!(standard_reported, vec![5, 10]);
3407 }
3408 }
3409
3410 #[test]
3411 fn test_from_config_records_whether_style_was_configured() {
3412 use crate::config::Config;
3416 use std::collections::BTreeMap;
3417
3418 let mut values = BTreeMap::new();
3419 values.insert("style".to_string(), toml::Value::String("indented".to_string()));
3420 let mut config = Config::default();
3421 config.rules.insert(
3422 "MD046".to_string(),
3423 crate::config::RuleConfig { severity: None, values },
3424 );
3425
3426 let configured = MD046CodeBlockStyle::from_config(&config);
3427 let configured = configured.as_any().downcast_ref::<MD046CodeBlockStyle>().unwrap();
3428 assert_eq!(configured.config.style, CodeBlockStyle::Indented);
3429 assert!(configured.style_explicit);
3430
3431 let defaulted = MD046CodeBlockStyle::from_config(&Config::default());
3432 let defaulted = defaulted.as_any().downcast_ref::<MD046CodeBlockStyle>().unwrap();
3433 assert!(!defaulted.style_explicit);
3434
3435 let indented = MD046CodeBlockStyle::from_config_struct(MD046Config {
3438 style: CodeBlockStyle::Indented,
3439 });
3440 let content = "# Feature: F\n\nText.\n\n code here\n";
3441 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3442 assert!(indented.fix(&mdg_ctx).unwrap().contains("```\n code here\n```"));
3443 }
3444
3445 #[test]
3446 fn test_mdg_indented_style_keeps_tables_out_of_code() {
3447 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
3450 let content = "# Feature: Eating\n\n#### Examples:\n\n | start | eat | left |\n | ----- | --- | ---- |\n";
3451
3452 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3453 assert!(rule.check(&mdg_ctx).unwrap().is_empty());
3454 assert_eq!(rule.fix(&mdg_ctx).unwrap(), content);
3455
3456 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3459 assert!(rule.check(&standard_ctx).unwrap().is_empty());
3460 assert_eq!(rule.fix(&standard_ctx).unwrap(), content);
3461 }
3462}