1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::utils::calculate_indentation_width_default;
3use crate::utils::mkdocs_admonitions;
4use crate::utils::mkdocs_tabs;
5use crate::utils::range_utils::calculate_line_range;
6use toml;
7
8mod md046_config;
9pub use md046_config::CodeBlockStyle;
10use md046_config::MD046Config;
11
12struct IndentContext<'a> {
14 in_list_context: &'a [bool],
15 in_tab_context: &'a [bool],
16 in_admonition_context: &'a [bool],
17 in_comment_or_html: &'a [bool],
27 list_item_baseline: &'a [Option<usize>],
37}
38
39#[derive(Clone)]
45pub struct MD046CodeBlockStyle {
46 config: MD046Config,
47}
48
49impl MD046CodeBlockStyle {
50 pub fn new(style: CodeBlockStyle) -> Self {
51 Self {
52 config: MD046Config { style },
53 }
54 }
55
56 pub fn from_config_struct(config: MD046Config) -> Self {
57 Self { config }
58 }
59
60 fn has_valid_fence_indent(line: &str) -> bool {
65 calculate_indentation_width_default(line) < 4
66 }
67
68 fn is_fenced_code_block_start(&self, line: &str) -> bool {
77 if !Self::has_valid_fence_indent(line) {
78 return false;
79 }
80
81 let trimmed = line.trim_start();
82 trimmed.starts_with("```") || trimmed.starts_with("~~~")
83 }
84
85 fn is_list_item(&self, line: &str) -> bool {
86 let trimmed = line.trim_start();
87 if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ") {
88 return true;
89 }
90 let after_digits = trimmed.trim_start_matches(|c: char| c.is_ascii_digit());
95 after_digits.len() < trimmed.len() && (after_digits.starts_with(". ") || after_digits.starts_with(") "))
96 }
97
98 fn is_footnote_definition(&self, line: &str) -> bool {
118 let trimmed = line.trim_start();
119 if !trimmed.starts_with("[^") || trimmed.len() < 5 {
120 return false;
121 }
122
123 if let Some(close_bracket_pos) = trimmed.find("]:")
124 && close_bracket_pos > 2
125 {
126 let label = &trimmed[2..close_bracket_pos];
127
128 if label.trim().is_empty() {
129 return false;
130 }
131
132 if label.contains('\r') {
134 return false;
135 }
136
137 if label.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
139 return true;
140 }
141 }
142
143 false
144 }
145
146 fn precompute_block_continuation_context(&self, lines: &[&str]) -> Vec<bool> {
169 let mut in_continuation_context = vec![false; lines.len()];
170 let mut last_list_item_line: Option<usize> = None;
171 let mut last_footnote_line: Option<usize> = None;
172 let mut blank_line_count = 0;
173
174 for (i, line) in lines.iter().enumerate() {
175 let trimmed = line.trim_start();
176 let indent_len = line.len() - trimmed.len();
177
178 if self.is_list_item(line) {
180 last_list_item_line = Some(i);
181 last_footnote_line = None; blank_line_count = 0;
183 in_continuation_context[i] = true;
184 continue;
185 }
186
187 if self.is_footnote_definition(line) {
189 last_footnote_line = Some(i);
190 last_list_item_line = None; blank_line_count = 0;
192 in_continuation_context[i] = true;
193 continue;
194 }
195
196 if line.trim().is_empty() {
198 if last_list_item_line.is_some() || last_footnote_line.is_some() {
200 blank_line_count += 1;
201 in_continuation_context[i] = true;
202
203 }
207 continue;
208 }
209
210 if indent_len == 0 && !trimmed.is_empty() {
212 if trimmed.starts_with('#') {
216 last_list_item_line = None;
217 last_footnote_line = None;
218 blank_line_count = 0;
219 continue;
220 }
221
222 if trimmed.starts_with("---") || trimmed.starts_with("***") {
224 last_list_item_line = None;
225 last_footnote_line = None;
226 blank_line_count = 0;
227 continue;
228 }
229
230 if let Some(list_line) = last_list_item_line
233 && (i - list_line > 5 || blank_line_count > 1)
234 {
235 last_list_item_line = None;
236 }
237
238 if last_footnote_line.is_some() {
240 last_footnote_line = None;
241 }
242
243 blank_line_count = 0;
244
245 if last_list_item_line.is_none() && last_footnote_line.is_some() {
247 last_footnote_line = None;
248 }
249 continue;
250 }
251
252 if indent_len > 0 && (last_list_item_line.is_some() || last_footnote_line.is_some()) {
254 in_continuation_context[i] = true;
255 blank_line_count = 0;
256 }
257 }
258
259 in_continuation_context
260 }
261
262 fn precompute_list_item_baseline(
273 &self,
274 ctx: &crate::lint_context::LintContext,
275 lines: &[&str],
276 ) -> Vec<Option<usize>> {
277 let mut baselines = vec![None; lines.len()];
278 let mut last_baseline: Option<usize> = None;
279 let mut last_list_item_line: Option<usize> = None;
280 let mut blank_line_count = 0usize;
281
282 for (i, line) in lines.iter().enumerate() {
283 let trimmed = line.trim_start();
284 let indent_len = line.len() - trimmed.len();
285
286 if let Some(item) = ctx.line_info(i + 1).and_then(|li| li.list_item.as_ref()) {
288 last_baseline = Some(item.content_column);
289 last_list_item_line = Some(i);
290 blank_line_count = 0;
291 baselines[i] = last_baseline;
292 continue;
293 }
294
295 if line.trim().is_empty() {
297 if last_baseline.is_some() {
298 blank_line_count += 1;
299 baselines[i] = last_baseline;
300 }
301 continue;
302 }
303
304 if indent_len == 0 {
308 if trimmed.starts_with('#') || trimmed.starts_with("---") || trimmed.starts_with("***") {
309 last_baseline = None;
310 last_list_item_line = None;
311 } else if let Some(list_line) = last_list_item_line
312 && (i - list_line > 5 || blank_line_count > 1)
313 {
314 last_baseline = None;
315 last_list_item_line = None;
316 }
317 blank_line_count = 0;
318 continue;
319 }
320
321 if last_baseline.is_some() {
323 baselines[i] = last_baseline;
324 blank_line_count = 0;
325 }
326 }
327
328 baselines
329 }
330
331 fn is_indented_code_block_with_context(
333 &self,
334 lines: &[&str],
335 i: usize,
336 is_mkdocs: bool,
337 ctx: &IndentContext,
338 ) -> bool {
339 if i >= lines.len() {
340 return false;
341 }
342
343 let line = lines[i];
344
345 let indent = calculate_indentation_width_default(line);
347 if indent < 4 {
348 return false;
349 }
350
351 if ctx.in_list_context[i] {
357 let crosses_baseline = ctx
358 .list_item_baseline
359 .get(i)
360 .copied()
361 .flatten()
362 .is_some_and(|base| indent >= base + 4);
363 if !crosses_baseline {
364 return false;
365 }
366 }
367
368 if is_mkdocs && ctx.in_tab_context[i] {
370 return false;
371 }
372
373 if is_mkdocs && ctx.in_admonition_context[i] {
376 return false;
377 }
378
379 if ctx.in_comment_or_html.get(i).copied().unwrap_or(false) {
385 return false;
386 }
387
388 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
393 let prev_is_indented_code = i > 0
394 && {
395 let prev_indent = calculate_indentation_width_default(lines[i - 1]);
396 if prev_indent < 4 {
397 false
398 } else if ctx.in_list_context[i - 1] {
399 ctx.list_item_baseline
400 .get(i - 1)
401 .copied()
402 .flatten()
403 .is_some_and(|base| prev_indent >= base + 4)
404 } else {
405 true
406 }
407 }
408 && !(is_mkdocs && ctx.in_tab_context[i - 1])
409 && !(is_mkdocs && ctx.in_admonition_context[i - 1])
410 && !ctx.in_comment_or_html.get(i - 1).copied().unwrap_or(false);
411
412 if !has_blank_line_before && !prev_is_indented_code {
415 return false;
416 }
417
418 true
419 }
420
421 fn precompute_comment_or_html_context(ctx: &crate::lint_context::LintContext, line_count: usize) -> Vec<bool> {
430 (0..line_count)
431 .map(|i| {
432 ctx.line_info(i + 1).is_some_and(|info| {
433 info.in_html_comment
434 || info.in_mdx_comment
435 || info.in_html_block
436 || info.in_jsx_block
437 || info.in_mkdocstrings
438 || info.in_footnote_definition
439 || info.blockquote.is_some()
440 })
441 })
442 .collect()
443 }
444
445 fn precompute_mkdocs_tab_context(&self, lines: &[&str]) -> Vec<bool> {
447 let mut in_tab_context = vec![false; lines.len()];
448 let mut current_tab_indent: Option<usize> = None;
449
450 for (i, line) in lines.iter().enumerate() {
451 if mkdocs_tabs::is_tab_marker(line) {
453 let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
454 current_tab_indent = Some(tab_indent);
455 in_tab_context[i] = true;
456 continue;
457 }
458
459 if let Some(tab_indent) = current_tab_indent {
461 if mkdocs_tabs::is_tab_content(line, tab_indent) {
462 in_tab_context[i] = true;
463 } else if !line.trim().is_empty() && calculate_indentation_width_default(line) < 4 {
464 current_tab_indent = None;
466 } else {
467 in_tab_context[i] = true;
469 }
470 }
471 }
472
473 in_tab_context
474 }
475
476 fn precompute_mkdocs_admonition_context(&self, lines: &[&str]) -> Vec<bool> {
485 let mut in_admonition_context = vec![false; lines.len()];
486 let mut admonition_stack: Vec<usize> = Vec::new();
488
489 for (i, line) in lines.iter().enumerate() {
490 let line_indent = calculate_indentation_width_default(line);
491
492 if mkdocs_admonitions::is_admonition_start(line) {
494 let adm_indent = mkdocs_admonitions::get_admonition_indent(line).unwrap_or(0);
495
496 while let Some(&top_indent) = admonition_stack.last() {
498 if adm_indent <= top_indent {
500 admonition_stack.pop();
501 } else {
502 break;
503 }
504 }
505
506 admonition_stack.push(adm_indent);
508 in_admonition_context[i] = true;
509 continue;
510 }
511
512 if line.trim().is_empty() {
514 if !admonition_stack.is_empty() {
515 in_admonition_context[i] = true;
516 }
517 continue;
518 }
519
520 while let Some(&top_indent) = admonition_stack.last() {
523 if line_indent >= top_indent + 4 {
525 break;
527 } else {
528 admonition_stack.pop();
530 }
531 }
532
533 if !admonition_stack.is_empty() {
535 in_admonition_context[i] = true;
536 }
537 }
538
539 in_admonition_context
540 }
541
542 fn categorize_indented_blocks(
554 &self,
555 lines: &[&str],
556 is_mkdocs: bool,
557 ictx: &IndentContext<'_>,
558 ) -> (Vec<bool>, Vec<bool>) {
559 let mut is_misplaced = vec![false; lines.len()];
560 let mut contains_fences = vec![false; lines.len()];
561
562 let mut i = 0;
564 while i < lines.len() {
565 if !self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx) {
567 i += 1;
568 continue;
569 }
570
571 let block_start = i;
573 let mut block_end = i;
574
575 while block_end < lines.len() && self.is_indented_code_block_with_context(lines, block_end, is_mkdocs, ictx)
576 {
577 block_end += 1;
578 }
579
580 if block_end > block_start {
582 let first_line = lines[block_start].trim_start();
583 let last_line = lines[block_end - 1].trim_start();
584
585 let is_backtick_fence = first_line.starts_with("```");
587 let is_tilde_fence = first_line.starts_with("~~~");
588
589 if is_backtick_fence || is_tilde_fence {
590 let fence_char = if is_backtick_fence { '`' } else { '~' };
591 let opener_len = first_line.chars().take_while(|&c| c == fence_char).count();
592
593 let closer_fence_len = last_line.chars().take_while(|&c| c == fence_char).count();
595 let after_closer = &last_line[closer_fence_len..];
596
597 if closer_fence_len >= opener_len && after_closer.trim().is_empty() {
598 is_misplaced[block_start..block_end].fill(true);
600 } else {
601 contains_fences[block_start..block_end].fill(true);
603 }
604 } else {
605 let has_fence_markers = (block_start..block_end).any(|j| {
608 let trimmed = lines[j].trim_start();
609 trimmed.starts_with("```") || trimmed.starts_with("~~~")
610 });
611
612 if has_fence_markers {
613 contains_fences[block_start..block_end].fill(true);
614 }
615 }
616 }
617
618 i = block_end;
619 }
620
621 (is_misplaced, contains_fences)
622 }
623
624 fn check_unclosed_code_blocks(&self, ctx: &crate::lint_context::LintContext) -> Vec<LintWarning> {
625 let mut warnings = Vec::new();
626 let lines = ctx.raw_lines();
627
628 let has_markdown_doc_block = ctx.code_block_details.iter().any(|d| {
630 if !d.is_fenced {
631 return false;
632 }
633 let lang = d.info_string.to_lowercase();
634 lang.starts_with("markdown") || lang.starts_with("md")
635 });
636
637 if has_markdown_doc_block {
640 return warnings;
641 }
642
643 for detail in &ctx.code_block_details {
644 if !detail.is_fenced {
645 continue;
646 }
647
648 if detail.end != ctx.content.len() {
650 continue;
651 }
652
653 let opening_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
655 Ok(idx) => idx,
656 Err(idx) => idx.saturating_sub(1),
657 };
658
659 let line = lines.get(opening_line_idx).unwrap_or(&"");
661 let trimmed = line.trim();
662 let fence_marker = if let Some(pos) = trimmed.find("```") {
663 let count = trimmed[pos..].chars().take_while(|&c| c == '`').count();
664 "`".repeat(count)
665 } else if let Some(pos) = trimmed.find("~~~") {
666 let count = trimmed[pos..].chars().take_while(|&c| c == '~').count();
667 "~".repeat(count)
668 } else {
669 "```".to_string()
670 };
671
672 let last_non_empty_line = lines.iter().rev().find(|l| !l.trim().is_empty()).unwrap_or(&"");
674 let last_trimmed = last_non_empty_line.trim();
675 let fence_char = fence_marker.chars().next().unwrap_or('`');
676
677 let has_closing_fence = if fence_char == '`' {
678 last_trimmed.starts_with("```") && {
679 let fence_len = last_trimmed.chars().take_while(|&c| c == '`').count();
680 last_trimmed[fence_len..].trim().is_empty()
681 }
682 } else {
683 last_trimmed.starts_with("~~~") && {
684 let fence_len = last_trimmed.chars().take_while(|&c| c == '~').count();
685 last_trimmed[fence_len..].trim().is_empty()
686 }
687 };
688
689 if !has_closing_fence {
690 if ctx
692 .lines
693 .get(opening_line_idx)
694 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
695 {
696 continue;
697 }
698
699 let (start_line, start_col, end_line, end_col) = calculate_line_range(opening_line_idx + 1, line);
700
701 warnings.push(LintWarning {
702 rule_name: Some(self.name().to_string()),
703 line: start_line,
704 column: start_col,
705 end_line,
706 end_column: end_col,
707 message: format!("Code block opened with '{fence_marker}' but never closed"),
708 severity: Severity::Warning,
709 fix: Some(Fix::new(
710 ctx.content.len()..ctx.content.len(),
711 format!("\n{fence_marker}"),
712 )),
713 });
714 }
715 }
716
717 warnings
718 }
719
720 fn detect_style(
721 &self,
722 ctx: &crate::lint_context::LintContext,
723 lines: &[&str],
724 is_mkdocs: bool,
725 ictx: &IndentContext,
726 ) -> Option<CodeBlockStyle> {
727 if lines.is_empty() {
728 return None;
729 }
730
731 let mut fenced_count = 0;
732 let mut indented_count = 0;
733
734 let mut in_fenced = false;
744 let mut prev_was_indented = false;
745
746 for (i, line) in lines.iter().enumerate() {
747 let in_container = ictx.in_comment_or_html.get(i).copied().unwrap_or(false);
748
749 if ctx.flavor.supports_colon_code_fences() && ctx.lines.get(i).is_some_and(|l| l.in_code_block) {
753 prev_was_indented = false;
754 continue;
755 }
756
757 if ctx.flavor.supports_myst_directives() && ctx.lines.get(i).is_some_and(|l| l.in_myst_directive) {
759 prev_was_indented = false;
760 continue;
761 }
762
763 if self.is_fenced_code_block_start(line) {
764 if in_container {
765 prev_was_indented = false;
768 continue;
769 }
770 if !in_fenced {
771 fenced_count += 1;
773 in_fenced = true;
774 } else {
775 in_fenced = false;
777 }
778 prev_was_indented = false;
779 } else if !in_fenced && self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx) {
780 if !prev_was_indented {
782 indented_count += 1;
783 }
784 prev_was_indented = true;
785 } else {
786 prev_was_indented = false;
787 }
788 }
789
790 if fenced_count == 0 && indented_count == 0 {
791 None
792 } else if fenced_count > 0 && indented_count == 0 {
793 Some(CodeBlockStyle::Fenced)
794 } else if fenced_count == 0 && indented_count > 0 {
795 Some(CodeBlockStyle::Indented)
796 } else if fenced_count >= indented_count {
797 Some(CodeBlockStyle::Fenced)
798 } else {
799 Some(CodeBlockStyle::Indented)
800 }
801 }
802}
803
804impl Rule for MD046CodeBlockStyle {
805 fn name(&self) -> &'static str {
806 "MD046"
807 }
808
809 fn description(&self) -> &'static str {
810 "Code blocks should use a consistent style"
811 }
812
813 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
814 if ctx.content.is_empty() {
816 return Ok(Vec::new());
817 }
818
819 if !ctx.content.contains("```")
821 && !ctx.content.contains("~~~")
822 && !ctx.content.contains(" ")
823 && !ctx.content.contains('\t')
824 {
825 return Ok(Vec::new());
826 }
827
828 let unclosed_warnings = self.check_unclosed_code_blocks(ctx);
830
831 if !unclosed_warnings.is_empty() {
833 return Ok(unclosed_warnings);
834 }
835
836 let lines = ctx.raw_lines();
838 let mut warnings = Vec::new();
839
840 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
841
842 let target_style = match self.config.style {
844 CodeBlockStyle::Consistent => {
845 let in_list_context = self.precompute_block_continuation_context(lines);
846 let list_item_baseline = self.precompute_list_item_baseline(ctx, lines);
847 let in_comment_or_html = Self::precompute_comment_or_html_context(ctx, lines.len());
848 let in_tab_context = if is_mkdocs {
849 self.precompute_mkdocs_tab_context(lines)
850 } else {
851 vec![false; lines.len()]
852 };
853 let in_admonition_context = if is_mkdocs {
854 self.precompute_mkdocs_admonition_context(lines)
855 } else {
856 vec![false; lines.len()]
857 };
858 let ictx = IndentContext {
859 in_list_context: &in_list_context,
860 in_tab_context: &in_tab_context,
861 in_admonition_context: &in_admonition_context,
862 in_comment_or_html: &in_comment_or_html,
863 list_item_baseline: &list_item_baseline,
864 };
865 self.detect_style(ctx, lines, is_mkdocs, &ictx)
866 .unwrap_or(CodeBlockStyle::Fenced)
867 }
868 _ => self.config.style,
869 };
870
871 let mut reported_indented_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
873
874 for detail in &ctx.code_block_details {
875 if detail.start >= ctx.content.len() || detail.end > ctx.content.len() {
876 continue;
877 }
878
879 let start_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
880 Ok(idx) => idx,
881 Err(idx) => idx.saturating_sub(1),
882 };
883
884 if detail.is_fenced {
885 if target_style == CodeBlockStyle::Indented {
886 let line = lines.get(start_line_idx).unwrap_or(&"");
887
888 if ctx
889 .lines
890 .get(start_line_idx)
891 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment || info.in_footnote_definition)
892 {
893 continue;
894 }
895
896 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
897 warnings.push(LintWarning {
898 rule_name: Some(self.name().to_string()),
899 line: start_line,
900 column: start_col,
901 end_line,
902 end_column: end_col,
903 message: "Use indented code blocks".to_string(),
904 severity: Severity::Warning,
905 fix: None,
906 });
907 }
908 } else {
909 if target_style == CodeBlockStyle::Fenced && !reported_indented_lines.contains(&start_line_idx) {
911 let line = lines.get(start_line_idx).unwrap_or(&"");
912
913 if ctx.lines.get(start_line_idx).is_some_and(|info| {
915 info.in_html_comment
916 || info.in_mdx_comment
917 || info.in_html_block
918 || info.in_jsx_block
919 || info.in_mkdocstrings
920 || info.in_footnote_definition
921 || info.blockquote.is_some()
922 }) {
923 continue;
924 }
925
926 if is_mkdocs
928 && ctx
929 .lines
930 .get(start_line_idx)
931 .is_some_and(|info| info.in_admonition || info.in_content_tab)
932 {
933 continue;
934 }
935
936 reported_indented_lines.insert(start_line_idx);
937
938 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
939 warnings.push(LintWarning {
940 rule_name: Some(self.name().to_string()),
941 line: start_line,
942 column: start_col,
943 end_line,
944 end_column: end_col,
945 message: "Use fenced code blocks".to_string(),
946 severity: Severity::Warning,
947 fix: None,
948 });
949 }
950 }
951 }
952
953 warnings.sort_by_key(|w| (w.line, w.column));
955
956 Ok(warnings)
957 }
958
959 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
960 let content = ctx.content;
961 if content.is_empty() {
962 return Ok(String::new());
963 }
964
965 let lines = ctx.raw_lines();
966
967 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
969
970 let in_comment_or_html = Self::precompute_comment_or_html_context(ctx, lines.len());
971
972 let in_list_context = self.precompute_block_continuation_context(lines);
974 let list_item_baseline = self.precompute_list_item_baseline(ctx, lines);
975 let in_tab_context = if is_mkdocs {
976 self.precompute_mkdocs_tab_context(lines)
977 } else {
978 vec![false; lines.len()]
979 };
980 let in_admonition_context = if is_mkdocs {
981 self.precompute_mkdocs_admonition_context(lines)
982 } else {
983 vec![false; lines.len()]
984 };
985
986 let ictx = IndentContext {
987 in_list_context: &in_list_context,
988 in_tab_context: &in_tab_context,
989 in_admonition_context: &in_admonition_context,
990 in_comment_or_html: &in_comment_or_html,
991 list_item_baseline: &list_item_baseline,
992 };
993
994 let target_style = match self.config.style {
995 CodeBlockStyle::Consistent => self
996 .detect_style(ctx, lines, is_mkdocs, &ictx)
997 .unwrap_or(CodeBlockStyle::Fenced),
998 _ => self.config.style,
999 };
1000
1001 let (misplaced_fence_lines, unsafe_fence_lines) = self.categorize_indented_blocks(lines, is_mkdocs, &ictx);
1005
1006 let mut result = String::with_capacity(content.len());
1007 let mut in_fenced_block = false;
1008 let mut fenced_fence_opener: Option<(char, usize)> = None;
1012 let mut in_indented_block = false;
1013 let mut current_block_fence_indent = String::new();
1018
1019 let mut current_block_disabled = false;
1021
1022 for (i, line) in lines.iter().enumerate() {
1023 let line_num = i + 1;
1024 let trimmed = line.trim_start();
1025
1026 if !in_fenced_block
1029 && Self::has_valid_fence_indent(line)
1030 && (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
1031 {
1032 current_block_disabled = ctx.inline_config().is_rule_disabled(self.name(), line_num);
1034 in_fenced_block = true;
1035 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
1036 let opener_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1037 fenced_fence_opener = Some((fence_char, opener_len));
1038
1039 if current_block_disabled {
1040 result.push_str(line);
1042 result.push('\n');
1043 } else if target_style == CodeBlockStyle::Indented {
1044 in_indented_block = true;
1046 } else {
1047 result.push_str(line);
1049 result.push('\n');
1050 }
1051 } else if in_fenced_block && fenced_fence_opener.is_some() {
1052 let (fence_char, opener_len) = fenced_fence_opener.unwrap();
1053 let closer_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1056 let after_closer = &trimmed[closer_len..];
1057 let is_closer = closer_len >= opener_len && after_closer.trim().is_empty() && closer_len > 0;
1058 if is_closer {
1059 in_fenced_block = false;
1060 fenced_fence_opener = None;
1061 in_indented_block = false;
1062
1063 if current_block_disabled {
1064 result.push_str(line);
1065 result.push('\n');
1066 } else if target_style == CodeBlockStyle::Indented {
1067 } else {
1069 result.push_str(line);
1071 result.push('\n');
1072 }
1073 current_block_disabled = false;
1074 } else if current_block_disabled {
1075 result.push_str(line);
1077 result.push('\n');
1078 } else if target_style == CodeBlockStyle::Indented {
1079 if !line.is_empty() {
1086 result.push_str(" ");
1087 result.push_str(line);
1088 }
1089 result.push('\n');
1090 } else {
1091 result.push_str(line);
1093 result.push('\n');
1094 }
1095 } else if self.is_indented_code_block_with_context(lines, i, is_mkdocs, &ictx) {
1096 if ctx.inline_config().is_rule_disabled(self.name(), line_num) {
1100 result.push_str(line);
1101 result.push('\n');
1102 continue;
1103 }
1104
1105 let prev_line_is_indented =
1107 i > 0 && self.is_indented_code_block_with_context(lines, i - 1, is_mkdocs, &ictx);
1108
1109 if target_style == CodeBlockStyle::Fenced {
1110 let baseline = list_item_baseline.get(i).copied().flatten().unwrap_or(0);
1116 let body = line.strip_prefix(" ").unwrap_or(line);
1122
1123 if misplaced_fence_lines[i] {
1126 result.push_str(line.trim_start());
1128 result.push('\n');
1129 } else if unsafe_fence_lines[i] {
1130 result.push_str(line);
1133 result.push('\n');
1134 } else if !prev_line_is_indented && !in_indented_block {
1135 current_block_fence_indent = " ".repeat(baseline);
1137 result.push_str(¤t_block_fence_indent);
1138 result.push_str("```\n");
1139 result.push_str(body);
1140 result.push('\n');
1141 in_indented_block = true;
1142 } else {
1143 result.push_str(body);
1145 result.push('\n');
1146 }
1147
1148 let next_line_is_indented =
1150 i < lines.len() - 1 && self.is_indented_code_block_with_context(lines, i + 1, is_mkdocs, &ictx);
1151 if !next_line_is_indented
1153 && in_indented_block
1154 && !misplaced_fence_lines[i]
1155 && !unsafe_fence_lines[i]
1156 {
1157 result.push_str(¤t_block_fence_indent);
1158 result.push_str("```\n");
1159 in_indented_block = false;
1160 current_block_fence_indent.clear();
1161 }
1162 } else {
1163 result.push_str(line);
1165 result.push('\n');
1166 }
1167 } else {
1168 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1170 result.push_str(¤t_block_fence_indent);
1171 result.push_str("```\n");
1172 in_indented_block = false;
1173 current_block_fence_indent.clear();
1174 }
1175
1176 result.push_str(line);
1177 result.push('\n');
1178 }
1179 }
1180
1181 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1183 result.push_str(¤t_block_fence_indent);
1184 result.push_str("```\n");
1185 }
1186
1187 if let Some((fence_char, opener_len)) = fenced_fence_opener
1193 && in_fenced_block
1194 {
1195 let has_unclosed_violation = !self.check_unclosed_code_blocks(ctx).is_empty();
1196 if has_unclosed_violation {
1197 let closer: String = std::iter::repeat_n(fence_char, opener_len).collect();
1198 result.push_str(&closer);
1199 result.push('\n');
1200 }
1201 }
1202
1203 if !content.ends_with('\n') && result.ends_with('\n') {
1205 result.pop();
1206 }
1207
1208 Ok(result)
1209 }
1210
1211 fn category(&self) -> RuleCategory {
1213 RuleCategory::CodeBlock
1214 }
1215
1216 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
1218 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
1221 }
1222
1223 fn as_any(&self) -> &dyn std::any::Any {
1224 self
1225 }
1226
1227 fn default_config_section(&self) -> Option<(String, toml::Value)> {
1228 let json_value = serde_json::to_value(&self.config).ok()?;
1229 Some((
1230 self.name().to_string(),
1231 crate::rule_config_serde::json_to_toml_value(&json_value)?,
1232 ))
1233 }
1234
1235 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1236 where
1237 Self: Sized,
1238 {
1239 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
1240 Box::new(Self::from_config_struct(rule_config))
1241 }
1242}
1243
1244#[cfg(test)]
1245mod tests {
1246 use super::*;
1247 use crate::lint_context::LintContext;
1248
1249 fn detect_style_from_content(rule: &MD046CodeBlockStyle, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
1261 let flavor = if is_mkdocs {
1262 crate::config::MarkdownFlavor::MkDocs
1263 } else {
1264 crate::config::MarkdownFlavor::Standard
1265 };
1266 let ctx = LintContext::new(content, flavor, None);
1267 let lines: Vec<&str> = content.lines().collect();
1268 let in_list_context = rule.precompute_block_continuation_context(&lines);
1269 let in_tab_context = if is_mkdocs {
1270 rule.precompute_mkdocs_tab_context(&lines)
1271 } else {
1272 vec![false; lines.len()]
1273 };
1274 let in_admonition_context = if is_mkdocs {
1275 rule.precompute_mkdocs_admonition_context(&lines)
1276 } else {
1277 vec![false; lines.len()]
1278 };
1279 let in_comment_or_html = vec![false; lines.len()];
1280 let list_item_baseline: Vec<Option<usize>> = vec![None; lines.len()];
1286 let ictx = IndentContext {
1287 in_list_context: &in_list_context,
1288 in_tab_context: &in_tab_context,
1289 in_admonition_context: &in_admonition_context,
1290 in_comment_or_html: &in_comment_or_html,
1291 list_item_baseline: &list_item_baseline,
1292 };
1293 rule.detect_style(&ctx, &lines, is_mkdocs, &ictx)
1294 }
1295
1296 #[test]
1297 fn test_fenced_code_block_detection() {
1298 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1299 assert!(rule.is_fenced_code_block_start("```"));
1300 assert!(rule.is_fenced_code_block_start("```rust"));
1301 assert!(rule.is_fenced_code_block_start("~~~"));
1302 assert!(rule.is_fenced_code_block_start("~~~python"));
1303 assert!(rule.is_fenced_code_block_start(" ```"));
1304 assert!(!rule.is_fenced_code_block_start("``"));
1305 assert!(!rule.is_fenced_code_block_start("~~"));
1306 assert!(!rule.is_fenced_code_block_start("Regular text"));
1307 }
1308
1309 #[test]
1310 fn test_consistent_style_with_fenced_blocks() {
1311 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1312 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
1313 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1314 let result = rule.check(&ctx).unwrap();
1315
1316 assert_eq!(result.len(), 0);
1318 }
1319
1320 #[test]
1321 fn test_consistent_style_with_indented_blocks() {
1322 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1323 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
1324 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1325 let result = rule.check(&ctx).unwrap();
1326
1327 assert_eq!(result.len(), 0);
1329 }
1330
1331 #[test]
1332 fn test_consistent_style_mixed() {
1333 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1334 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
1335 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1336 let result = rule.check(&ctx).unwrap();
1337
1338 assert!(!result.is_empty());
1340 }
1341
1342 #[test]
1343 fn test_fenced_style_with_indented_blocks() {
1344 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1345 let content = "Text\n\n indented code\n more code\n\nMore text";
1346 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1347 let result = rule.check(&ctx).unwrap();
1348
1349 assert!(!result.is_empty());
1351 assert!(result[0].message.contains("Use fenced code blocks"));
1352 }
1353
1354 #[test]
1355 fn test_fenced_style_with_tab_indented_blocks() {
1356 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1357 let content = "Text\n\n\ttab indented code\n\tmore code\n\nMore text";
1358 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1359 let result = rule.check(&ctx).unwrap();
1360
1361 assert!(!result.is_empty());
1363 assert!(result[0].message.contains("Use fenced code blocks"));
1364 }
1365
1366 #[test]
1367 fn test_fenced_style_with_mixed_whitespace_indented_blocks() {
1368 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1369 let content = "Text\n\n \tmixed indent code\n \tmore code\n\nMore text";
1371 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1372 let result = rule.check(&ctx).unwrap();
1373
1374 assert!(
1376 !result.is_empty(),
1377 "Mixed whitespace (2 spaces + tab) should be detected as indented code"
1378 );
1379 assert!(result[0].message.contains("Use fenced code blocks"));
1380 }
1381
1382 #[test]
1383 fn test_fenced_style_with_one_space_tab_indent() {
1384 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1385 let content = "Text\n\n \ttab after one space\n \tmore code\n\nMore text";
1387 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1388 let result = rule.check(&ctx).unwrap();
1389
1390 assert!(!result.is_empty(), "1 space + tab should be detected as indented code");
1391 assert!(result[0].message.contains("Use fenced code blocks"));
1392 }
1393
1394 #[test]
1395 fn test_indented_style_with_fenced_blocks() {
1396 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1397 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
1398 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1399 let result = rule.check(&ctx).unwrap();
1400
1401 assert!(!result.is_empty());
1403 assert!(result[0].message.contains("Use indented code blocks"));
1404 }
1405
1406 #[test]
1407 fn test_unclosed_code_block() {
1408 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1409 let content = "```\ncode without closing fence";
1410 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1411 let result = rule.check(&ctx).unwrap();
1412
1413 assert_eq!(result.len(), 1);
1414 assert!(result[0].message.contains("never closed"));
1415 }
1416
1417 #[test]
1418 fn test_nested_code_blocks() {
1419 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1420 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
1421 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1422 let result = rule.check(&ctx).unwrap();
1423
1424 assert_eq!(result.len(), 0);
1426 }
1427
1428 #[test]
1429 fn test_fix_indented_to_fenced() {
1430 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1431 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
1432 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1433 let fixed = rule.fix(&ctx).unwrap();
1434
1435 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
1436 }
1437
1438 #[test]
1439 fn test_fix_fenced_to_indented() {
1440 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1441 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
1442 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1443 let fixed = rule.fix(&ctx).unwrap();
1444
1445 assert!(fixed.contains(" code line 1\n code line 2"));
1446 assert!(!fixed.contains("```"));
1447 }
1448
1449 #[test]
1450 fn test_fix_fenced_to_indented_blank_lines_have_no_trailing_spaces() {
1451 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1455 let content = "Text\n\n```\ncode line 1\n\ncode line 2\n```\n\nMore text";
1456 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1457 let fixed = rule.fix(&ctx).unwrap();
1458
1459 for line in fixed.lines() {
1460 assert!(
1461 line.is_empty() || !line.trim_end().is_empty() || line == line.trim_end(),
1462 "no line may have trailing whitespace, got {line:?}"
1463 );
1464 assert_ne!(line, " ", "blank line was indented to trailing spaces");
1465 }
1466 assert!(fixed.contains(" code line 1\n\n code line 2"));
1468 }
1469
1470 #[test]
1471 fn test_is_list_item_requires_delimiter_after_digits() {
1472 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1473 assert!(rule.is_list_item("1. First"));
1475 assert!(rule.is_list_item("42) Item"));
1476 assert!(rule.is_list_item(" 3. Indented item"));
1477 assert!(rule.is_list_item("- bullet"));
1479 assert!(rule.is_list_item("* bullet"));
1480 assert!(!rule.is_list_item("2 results. More info."));
1483 assert!(!rule.is_list_item("3 options (a, b) here"));
1484 assert!(!rule.is_list_item("100 items in stock. Buy now"));
1485 }
1486
1487 #[test]
1488 fn test_fix_fenced_to_indented_preserves_internal_indentation() {
1489 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1492 let content = r#"# Test
1493
1494```html
1495<!doctype html>
1496<html>
1497 <head>
1498 <title>Test</title>
1499 </head>
1500</html>
1501```
1502"#;
1503 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1504 let fixed = rule.fix(&ctx).unwrap();
1505
1506 assert!(
1509 fixed.contains(" <head>"),
1510 "Expected 6 spaces before <head> (4 for code block + 2 original), got:\n{fixed}"
1511 );
1512 assert!(
1513 fixed.contains(" <title>"),
1514 "Expected 8 spaces before <title> (4 for code block + 4 original), got:\n{fixed}"
1515 );
1516 assert!(!fixed.contains("```"), "Fenced markers should be removed");
1517 }
1518
1519 #[test]
1520 fn test_fix_fenced_to_indented_preserves_python_indentation() {
1521 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1523 let content = r#"# Python Example
1524
1525```python
1526def greet(name):
1527 if name:
1528 print(f"Hello, {name}!")
1529 else:
1530 print("Hello, World!")
1531```
1532"#;
1533 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1534 let fixed = rule.fix(&ctx).unwrap();
1535
1536 assert!(
1538 fixed.contains(" def greet(name):"),
1539 "Function def should have 4 spaces (code block indent)"
1540 );
1541 assert!(
1542 fixed.contains(" if name:"),
1543 "if statement should have 8 spaces (4 code + 4 Python)"
1544 );
1545 assert!(
1546 fixed.contains(" print"),
1547 "print should have 12 spaces (4 code + 8 Python)"
1548 );
1549 }
1550
1551 #[test]
1552 fn test_fix_fenced_to_indented_preserves_yaml_indentation() {
1553 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1555 let content = r#"# Config
1556
1557```yaml
1558server:
1559 host: localhost
1560 port: 8080
1561 ssl:
1562 enabled: true
1563 cert: /path/to/cert
1564```
1565"#;
1566 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1567 let fixed = rule.fix(&ctx).unwrap();
1568
1569 assert!(fixed.contains(" server:"), "Root key should have 4 spaces");
1570 assert!(fixed.contains(" host:"), "First level should have 6 spaces");
1571 assert!(fixed.contains(" ssl:"), "ssl key should have 6 spaces");
1572 assert!(fixed.contains(" enabled:"), "Nested ssl should have 8 spaces");
1573 }
1574
1575 #[test]
1576 fn test_fix_fenced_to_indented_preserves_empty_lines() {
1577 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1581 let content = "```\nline1\n\nline2\n```\n";
1582 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1583 let fixed = rule.fix(&ctx).unwrap();
1584
1585 assert!(fixed.contains(" line1"), "line1 should be indented");
1587 assert!(fixed.contains(" line2"), "line2 should be indented");
1588 assert!(
1589 fixed.contains(" line1\n\n line2"),
1590 "blank line must stay empty, got {fixed:?}"
1591 );
1592 }
1593
1594 #[test]
1595 fn test_fix_fenced_to_indented_multiple_blocks() {
1596 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1598 let content = r#"# Doc
1599
1600```python
1601def foo():
1602 pass
1603```
1604
1605Text between.
1606
1607```yaml
1608key:
1609 value: 1
1610```
1611"#;
1612 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1613 let fixed = rule.fix(&ctx).unwrap();
1614
1615 assert!(fixed.contains(" def foo():"), "Python def should be indented");
1616 assert!(fixed.contains(" pass"), "Python body should have 8 spaces");
1617 assert!(fixed.contains(" key:"), "YAML root should have 4 spaces");
1618 assert!(fixed.contains(" value:"), "YAML nested should have 6 spaces");
1619 assert!(!fixed.contains("```"), "No fence markers should remain");
1620 }
1621
1622 #[test]
1623 fn test_fix_unclosed_block() {
1624 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1625 let content = "```\ncode without closing";
1626 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1627 let fixed = rule.fix(&ctx).unwrap();
1628
1629 assert!(fixed.ends_with("```"));
1631 }
1632
1633 #[test]
1634 fn test_code_block_in_list() {
1635 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1636 let content = "- List item\n code in list\n more code\n- Next item";
1637 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1638 let result = rule.check(&ctx).unwrap();
1639
1640 assert_eq!(result.len(), 0);
1642 }
1643
1644 #[test]
1645 fn test_detect_style_fenced() {
1646 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1647 let content = "```\ncode\n```";
1648 let style = detect_style_from_content(&rule, content, false);
1649
1650 assert_eq!(style, Some(CodeBlockStyle::Fenced));
1651 }
1652
1653 #[test]
1654 fn test_detect_style_indented() {
1655 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1656 let content = "Text\n\n code\n\nMore";
1657 let style = detect_style_from_content(&rule, content, false);
1658
1659 assert_eq!(style, Some(CodeBlockStyle::Indented));
1660 }
1661
1662 #[test]
1663 fn test_detect_style_none() {
1664 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1665 let content = "No code blocks here";
1666 let style = detect_style_from_content(&rule, content, false);
1667
1668 assert_eq!(style, None);
1669 }
1670
1671 #[test]
1672 fn test_tilde_fence() {
1673 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1674 let content = "~~~\ncode\n~~~";
1675 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1676 let result = rule.check(&ctx).unwrap();
1677
1678 assert_eq!(result.len(), 0);
1680 }
1681
1682 #[test]
1683 fn test_language_specification() {
1684 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1685 let content = "```rust\nfn main() {}\n```";
1686 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1687 let result = rule.check(&ctx).unwrap();
1688
1689 assert_eq!(result.len(), 0);
1690 }
1691
1692 #[test]
1693 fn test_empty_content() {
1694 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1695 let content = "";
1696 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1697 let result = rule.check(&ctx).unwrap();
1698
1699 assert_eq!(result.len(), 0);
1700 }
1701
1702 #[test]
1703 fn test_default_config() {
1704 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1705 let (name, _config) = rule.default_config_section().unwrap();
1706 assert_eq!(name, "MD046");
1707 }
1708
1709 #[test]
1710 fn test_markdown_documentation_block() {
1711 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1712 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
1713 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1714 let result = rule.check(&ctx).unwrap();
1715
1716 assert_eq!(result.len(), 0);
1718 }
1719
1720 #[test]
1721 fn test_preserve_trailing_newline() {
1722 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1723 let content = "```\ncode\n```\n";
1724 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1725 let fixed = rule.fix(&ctx).unwrap();
1726
1727 assert_eq!(fixed, content);
1728 }
1729
1730 #[test]
1731 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
1732 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1733 let content = r#"# Document
1734
1735=== "Python"
1736
1737 This is tab content
1738 Not an indented code block
1739
1740 ```python
1741 def hello():
1742 print("Hello")
1743 ```
1744
1745=== "JavaScript"
1746
1747 More tab content here
1748 Also not an indented code block"#;
1749
1750 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1751 let result = rule.check(&ctx).unwrap();
1752
1753 assert_eq!(result.len(), 0);
1755 }
1756
1757 #[test]
1758 fn test_mkdocs_tabs_with_actual_indented_code() {
1759 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1760 let content = r#"# Document
1761
1762=== "Tab 1"
1763
1764 This is tab content
1765
1766Regular text
1767
1768 This is an actual indented code block
1769 Should be flagged"#;
1770
1771 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1772 let result = rule.check(&ctx).unwrap();
1773
1774 assert_eq!(result.len(), 1);
1776 assert!(result[0].message.contains("Use fenced code blocks"));
1777 }
1778
1779 #[test]
1780 fn test_mkdocs_tabs_detect_style() {
1781 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1782 let content = r#"=== "Tab 1"
1783
1784 Content in tab
1785 More content
1786
1787=== "Tab 2"
1788
1789 Content in second tab"#;
1790
1791 let style = detect_style_from_content(&rule, content, true);
1793 assert_eq!(style, None); let style = detect_style_from_content(&rule, content, false);
1797 assert_eq!(style, Some(CodeBlockStyle::Indented));
1798 }
1799
1800 #[test]
1801 fn test_mkdocs_nested_tabs() {
1802 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1803 let content = r#"# Document
1804
1805=== "Outer Tab"
1806
1807 Some content
1808
1809 === "Nested Tab"
1810
1811 Nested tab content
1812 Should not be flagged"#;
1813
1814 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1815 let result = rule.check(&ctx).unwrap();
1816
1817 assert_eq!(result.len(), 0);
1819 }
1820
1821 #[test]
1822 fn test_mkdocs_admonitions_not_flagged_as_indented_code() {
1823 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1826 let content = r#"# Document
1827
1828!!! note
1829 This is normal admonition content, not a code block.
1830 It spans multiple lines.
1831
1832??? warning "Collapsible Warning"
1833 This is also admonition content.
1834
1835???+ tip "Expanded Tip"
1836 And this one too.
1837
1838Regular text outside admonitions."#;
1839
1840 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1841 let result = rule.check(&ctx).unwrap();
1842
1843 assert_eq!(
1845 result.len(),
1846 0,
1847 "Admonition content in MkDocs mode should not trigger MD046"
1848 );
1849 }
1850
1851 #[test]
1852 fn test_mkdocs_admonition_with_actual_indented_code() {
1853 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1855 let content = r#"# Document
1856
1857!!! note
1858 This is admonition content.
1859
1860Regular text ends the admonition.
1861
1862 This is actual indented code (should be flagged)"#;
1863
1864 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1865 let result = rule.check(&ctx).unwrap();
1866
1867 assert_eq!(result.len(), 1);
1869 assert!(result[0].message.contains("Use fenced code blocks"));
1870 }
1871
1872 #[test]
1873 fn test_admonition_in_standard_mode_flagged() {
1874 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1878 let content = r#"# Document
1879
1880!!! note
1881
1882 This looks like code in standard mode.
1883
1884Regular text."#;
1885
1886 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1888 let result = rule.check(&ctx).unwrap();
1889
1890 assert_eq!(
1892 result.len(),
1893 1,
1894 "Admonition content in Standard mode should be flagged as indented code"
1895 );
1896 }
1897
1898 #[test]
1899 fn test_mkdocs_admonition_with_fenced_code_inside() {
1900 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1902 let content = r#"# Document
1903
1904!!! note "Code Example"
1905 Here's some code:
1906
1907 ```python
1908 def hello():
1909 print("world")
1910 ```
1911
1912 More text after code.
1913
1914Regular text."#;
1915
1916 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1917 let result = rule.check(&ctx).unwrap();
1918
1919 assert_eq!(result.len(), 0, "Fenced code blocks inside admonitions should be valid");
1921 }
1922
1923 #[test]
1924 fn test_mkdocs_nested_admonitions() {
1925 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1927 let content = r#"# Document
1928
1929!!! note "Outer"
1930 Outer content.
1931
1932 !!! warning "Inner"
1933 Inner content.
1934 More inner content.
1935
1936 Back to outer.
1937
1938Regular text."#;
1939
1940 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1941 let result = rule.check(&ctx).unwrap();
1942
1943 assert_eq!(result.len(), 0, "Nested admonitions should not be flagged");
1945 }
1946
1947 #[test]
1948 fn test_mkdocs_admonition_fix_does_not_wrap() {
1949 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1951 let content = r#"!!! note
1952 Content that should stay as admonition content.
1953 Not be wrapped in code fences.
1954"#;
1955
1956 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1957 let fixed = rule.fix(&ctx).unwrap();
1958
1959 assert!(
1961 !fixed.contains("```\n Content"),
1962 "Admonition content should not be wrapped in fences"
1963 );
1964 assert_eq!(fixed, content, "Content should remain unchanged");
1965 }
1966
1967 #[test]
1968 fn test_mkdocs_empty_admonition() {
1969 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1971 let content = r#"!!! note
1972
1973Regular paragraph after empty admonition.
1974
1975 This IS an indented code block (after blank + non-indented line)."#;
1976
1977 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1978 let result = rule.check(&ctx).unwrap();
1979
1980 assert_eq!(result.len(), 1, "Indented code after admonition ends should be flagged");
1982 }
1983
1984 #[test]
1985 fn test_mkdocs_indented_admonition() {
1986 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1988 let content = r#"- List item
1989
1990 !!! note
1991 Indented admonition content.
1992 More content.
1993
1994- Next item"#;
1995
1996 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1997 let result = rule.check(&ctx).unwrap();
1998
1999 assert_eq!(
2001 result.len(),
2002 0,
2003 "Indented admonitions (e.g., in lists) should not be flagged"
2004 );
2005 }
2006
2007 #[test]
2008 fn test_footnote_indented_paragraphs_not_flagged() {
2009 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2010 let content = r#"# Test Document with Footnotes
2011
2012This is some text with a footnote[^1].
2013
2014Here's some code:
2015
2016```bash
2017echo "fenced code block"
2018```
2019
2020More text with another footnote[^2].
2021
2022[^1]: Really interesting footnote text.
2023
2024 Even more interesting second paragraph.
2025
2026[^2]: Another footnote.
2027
2028 With a second paragraph too.
2029
2030 And even a third paragraph!"#;
2031
2032 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2033 let result = rule.check(&ctx).unwrap();
2034
2035 assert_eq!(result.len(), 0);
2037 }
2038
2039 #[test]
2040 fn test_footnote_definition_detection() {
2041 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2042
2043 assert!(rule.is_footnote_definition("[^1]: Footnote text"));
2046 assert!(rule.is_footnote_definition("[^foo]: Footnote text"));
2047 assert!(rule.is_footnote_definition("[^long-name]: Footnote text"));
2048 assert!(rule.is_footnote_definition("[^test_123]: Mixed chars"));
2049 assert!(rule.is_footnote_definition(" [^1]: Indented footnote"));
2050 assert!(rule.is_footnote_definition("[^a]: Minimal valid footnote"));
2051 assert!(rule.is_footnote_definition("[^123]: Numeric label"));
2052 assert!(rule.is_footnote_definition("[^_]: Single underscore"));
2053 assert!(rule.is_footnote_definition("[^-]: Single hyphen"));
2054
2055 assert!(!rule.is_footnote_definition("[^]: No label"));
2057 assert!(!rule.is_footnote_definition("[^ ]: Whitespace only"));
2058 assert!(!rule.is_footnote_definition("[^ ]: Multiple spaces"));
2059 assert!(!rule.is_footnote_definition("[^\t]: Tab only"));
2060
2061 assert!(!rule.is_footnote_definition("[^]]: Extra bracket"));
2063 assert!(!rule.is_footnote_definition("Regular text [^1]:"));
2064 assert!(!rule.is_footnote_definition("[1]: Not a footnote"));
2065 assert!(!rule.is_footnote_definition("[^")); assert!(!rule.is_footnote_definition("[^1:")); assert!(!rule.is_footnote_definition("^1]: Missing opening bracket"));
2068
2069 assert!(!rule.is_footnote_definition("[^test.name]: Period"));
2071 assert!(!rule.is_footnote_definition("[^test name]: Space in label"));
2072 assert!(!rule.is_footnote_definition("[^test@name]: Special char"));
2073 assert!(!rule.is_footnote_definition("[^test/name]: Slash"));
2074 assert!(!rule.is_footnote_definition("[^test\\name]: Backslash"));
2075
2076 assert!(!rule.is_footnote_definition("[^test\r]: Carriage return"));
2079 }
2080
2081 #[test]
2082 fn test_footnote_with_blank_lines() {
2083 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2087 let content = r#"# Document
2088
2089Text with footnote[^1].
2090
2091[^1]: First paragraph.
2092
2093 Second paragraph after blank line.
2094
2095 Third paragraph after another blank line.
2096
2097Regular text at column 0 ends the footnote."#;
2098
2099 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2100 let result = rule.check(&ctx).unwrap();
2101
2102 assert_eq!(
2104 result.len(),
2105 0,
2106 "Indented content within footnotes should not trigger MD046"
2107 );
2108 }
2109
2110 #[test]
2111 fn test_footnote_multiple_consecutive_blank_lines() {
2112 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2115 let content = r#"Text[^1].
2116
2117[^1]: First paragraph.
2118
2119
2120
2121 Content after three blank lines (still part of footnote).
2122
2123Not indented, so footnote ends here."#;
2124
2125 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2126 let result = rule.check(&ctx).unwrap();
2127
2128 assert_eq!(
2130 result.len(),
2131 0,
2132 "Multiple blank lines shouldn't break footnote continuation"
2133 );
2134 }
2135
2136 #[test]
2137 fn test_footnote_terminated_by_non_indented_content() {
2138 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2141 let content = r#"[^1]: Footnote content.
2142
2143 More indented content in footnote.
2144
2145This paragraph is not indented, so footnote ends.
2146
2147 This should be flagged as indented code block."#;
2148
2149 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2150 let result = rule.check(&ctx).unwrap();
2151
2152 assert_eq!(
2154 result.len(),
2155 1,
2156 "Indented code after footnote termination should be flagged"
2157 );
2158 assert!(
2159 result[0].message.contains("Use fenced code blocks"),
2160 "Expected MD046 warning for indented code block"
2161 );
2162 assert!(result[0].line >= 7, "Warning should be on the indented code block line");
2163 }
2164
2165 #[test]
2166 fn test_footnote_terminated_by_structural_elements() {
2167 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2169 let content = r#"[^1]: Footnote content.
2170
2171 More content.
2172
2173## Heading terminates footnote
2174
2175 This indented content should be flagged.
2176
2177---
2178
2179 This should also be flagged (after horizontal rule)."#;
2180
2181 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2182 let result = rule.check(&ctx).unwrap();
2183
2184 assert_eq!(
2186 result.len(),
2187 2,
2188 "Both indented blocks after termination should be flagged"
2189 );
2190 }
2191
2192 #[test]
2193 fn test_footnote_with_code_block_inside() {
2194 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2197 let content = r#"Text[^1].
2198
2199[^1]: Footnote with code:
2200
2201 ```python
2202 def hello():
2203 print("world")
2204 ```
2205
2206 More footnote text after code."#;
2207
2208 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2209 let result = rule.check(&ctx).unwrap();
2210
2211 assert_eq!(result.len(), 0, "Fenced code blocks within footnotes should be allowed");
2213 }
2214
2215 #[test]
2216 fn test_footnote_with_8_space_indented_code() {
2217 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2220 let content = r#"Text[^1].
2221
2222[^1]: Footnote with nested code.
2223
2224 code block
2225 more code"#;
2226
2227 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2228 let result = rule.check(&ctx).unwrap();
2229
2230 assert_eq!(
2232 result.len(),
2233 0,
2234 "8-space indented code within footnotes represents nested code blocks"
2235 );
2236 }
2237
2238 #[test]
2239 fn test_multiple_footnotes() {
2240 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2243 let content = r#"Text[^1] and more[^2].
2244
2245[^1]: First footnote.
2246
2247 Continuation of first.
2248
2249[^2]: Second footnote starts here, ending the first.
2250
2251 Continuation of second."#;
2252
2253 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2254 let result = rule.check(&ctx).unwrap();
2255
2256 assert_eq!(
2258 result.len(),
2259 0,
2260 "Multiple footnotes should each maintain their continuation context"
2261 );
2262 }
2263
2264 #[test]
2265 fn test_list_item_ends_footnote_context() {
2266 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2268 let content = r#"[^1]: Footnote.
2269
2270 Content in footnote.
2271
2272- List item starts here (ends footnote context).
2273
2274 This indented content is part of the list, not the footnote."#;
2275
2276 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2277 let result = rule.check(&ctx).unwrap();
2278
2279 assert_eq!(
2281 result.len(),
2282 0,
2283 "List items should end footnote context and start their own"
2284 );
2285 }
2286
2287 #[test]
2288 fn test_footnote_vs_actual_indented_code() {
2289 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2292 let content = r#"# Heading
2293
2294Text with footnote[^1].
2295
2296[^1]: Footnote content.
2297
2298 Part of footnote (should not be flagged).
2299
2300Regular paragraph ends footnote context.
2301
2302 This is actual indented code (MUST be flagged)
2303 Should be detected as code block"#;
2304
2305 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2306 let result = rule.check(&ctx).unwrap();
2307
2308 assert_eq!(
2310 result.len(),
2311 1,
2312 "Must still detect indented code blocks outside footnotes"
2313 );
2314 assert!(
2315 result[0].message.contains("Use fenced code blocks"),
2316 "Expected MD046 warning for indented code"
2317 );
2318 assert!(
2319 result[0].line >= 11,
2320 "Warning should be on the actual indented code line"
2321 );
2322 }
2323
2324 #[test]
2325 fn test_spec_compliant_label_characters() {
2326 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2329
2330 assert!(rule.is_footnote_definition("[^test]: text"));
2332 assert!(rule.is_footnote_definition("[^TEST]: text"));
2333 assert!(rule.is_footnote_definition("[^test-name]: text"));
2334 assert!(rule.is_footnote_definition("[^test_name]: text"));
2335 assert!(rule.is_footnote_definition("[^test123]: text"));
2336 assert!(rule.is_footnote_definition("[^123]: text"));
2337 assert!(rule.is_footnote_definition("[^a1b2c3]: text"));
2338
2339 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")); }
2347
2348 #[test]
2349 fn test_code_block_inside_html_comment() {
2350 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2353 let content = r#"# Document
2354
2355Some text.
2356
2357<!--
2358Example code block in comment:
2359
2360```typescript
2361console.log("Hello");
2362```
2363
2364More comment text.
2365-->
2366
2367More content."#;
2368
2369 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2370 let result = rule.check(&ctx).unwrap();
2371
2372 assert_eq!(
2373 result.len(),
2374 0,
2375 "Code blocks inside HTML comments should not be flagged as unclosed"
2376 );
2377 }
2378
2379 #[test]
2380 fn test_unclosed_fence_inside_html_comment() {
2381 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2383 let content = r#"# Document
2384
2385<!--
2386Example with intentionally unclosed fence:
2387
2388```
2389code without closing
2390-->
2391
2392More content."#;
2393
2394 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2395 let result = rule.check(&ctx).unwrap();
2396
2397 assert_eq!(
2398 result.len(),
2399 0,
2400 "Unclosed fences inside HTML comments should be ignored"
2401 );
2402 }
2403
2404 #[test]
2405 fn test_multiline_html_comment_with_indented_code() {
2406 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2408 let content = r#"# Document
2409
2410<!--
2411Example:
2412
2413 indented code
2414 more code
2415
2416End of comment.
2417-->
2418
2419Regular text."#;
2420
2421 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2422 let result = rule.check(&ctx).unwrap();
2423
2424 assert_eq!(
2425 result.len(),
2426 0,
2427 "Indented code inside HTML comments should not be flagged"
2428 );
2429 }
2430
2431 #[test]
2432 fn test_code_block_after_html_comment() {
2433 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2435 let content = r#"# Document
2436
2437<!-- comment -->
2438
2439Text before.
2440
2441 indented code should be flagged
2442
2443More text."#;
2444
2445 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2446 let result = rule.check(&ctx).unwrap();
2447
2448 assert_eq!(
2449 result.len(),
2450 1,
2451 "Code blocks after HTML comments should still be detected"
2452 );
2453 assert!(result[0].message.contains("Use fenced code blocks"));
2454 }
2455
2456 #[test]
2457 fn test_consistent_style_indented_html_comment() {
2458 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2464 let content = "# MD046 false-positive reproduction\n\
2465 \n\
2466 <!--\n \
2467 This is just an indented comment, not a code block.\n\
2468 \n \
2469 A second line is required to trigger the false-positive.\n\
2470 \n \
2471 Actually, three lines are required.\n\
2472 -->\n\
2473 \n\
2474 ```md\n\
2475 This should be fine, since it's the only code block and therefore consistent.\n\
2476 ```\n";
2477
2478 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2479 let result = rule.check(&ctx).unwrap();
2480
2481 assert_eq!(
2482 result,
2483 vec![],
2484 "A single fenced block and an indented HTML comment must produce no MD046 warnings",
2485 );
2486 }
2487
2488 #[test]
2489 fn test_consistent_style_indented_html_block() {
2490 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2497 let content = "# Heading\n\
2498 \n\
2499 <div class=\"note\">\n \
2500 line one of indented html content\n \
2501 line two of indented html content\n \
2502 line three of indented html content\n\
2503 </div>\n\
2504 \n\
2505 ```md\n\
2506 real fenced block\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 "Indented content inside a raw HTML block must not influence MD046 style detection",
2516 );
2517 }
2518
2519 #[test]
2520 fn test_consistent_style_fake_fence_inside_html_comment() {
2521 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2527 let content = "# Title\n\
2528 \n\
2529 <!--\n\
2530 ```\n\
2531 fake fence inside comment\n\
2532 ```\n\
2533 -->\n\
2534 \n \
2535 real indented code block line 1\n \
2536 real indented code block line 2\n";
2537
2538 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2539 let result = rule.check(&ctx).unwrap();
2540
2541 assert_eq!(
2542 result,
2543 vec![],
2544 "Fence markers inside an HTML comment must not influence MD046 style detection",
2545 );
2546 }
2547
2548 #[test]
2549 fn test_consistent_style_indented_footnote_definition() {
2550 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2554 let content = "# Heading\n\
2555 \n\
2556 Reference to a footnote[^note].\n\
2557 \n\
2558 [^note]: First line of the footnote.\n \
2559 Second indented continuation line.\n \
2560 Third indented continuation line.\n \
2561 Fourth indented continuation line.\n\
2562 \n\
2563 ```md\n\
2564 real fenced block\n\
2565 ```\n";
2566
2567 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2568 let result = rule.check(&ctx).unwrap();
2569
2570 assert_eq!(
2571 result,
2572 vec![],
2573 "Footnote-definition continuation content must not influence MD046 style detection",
2574 );
2575 }
2576
2577 #[test]
2578 fn test_consistent_style_indented_blockquote() {
2579 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2584 let content = "# Heading\n\
2585 \n\
2586 > line one of quoted indented content\n\
2587 >\n\
2588 > line two of quoted indented content\n\
2589 >\n\
2590 > line three of quoted indented content\n\
2591 \n\
2592 ```md\n\
2593 real fenced block\n\
2594 ```\n";
2595
2596 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2597 let result = rule.check(&ctx).unwrap();
2598
2599 assert_eq!(
2600 result,
2601 vec![],
2602 "Indented content inside a blockquote must not influence MD046 style detection",
2603 );
2604 }
2605
2606 #[test]
2607 fn test_consistent_style_genuine_indented_block_detected_as_indented() {
2608 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2613 let content = "# Heading\n\
2614 \n\
2615 Some prose.\n\
2616 \n \
2617 real indented code line 1\n \
2618 real indented code line 2\n";
2619
2620 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2621 let result = rule.check(&ctx).unwrap();
2622
2623 assert_eq!(
2626 result,
2627 vec![],
2628 "A genuine top-level indented block must be detected as Indented style under Consistent",
2629 );
2630 }
2631
2632 #[test]
2633 fn test_consistent_style_skipped_lines_dont_override_real_block() {
2634 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2639 let content = "# Heading\n\
2640 \n\
2641 <!--\n \
2642 skipped indented comment line 1\n \
2643 skipped indented comment line 2\n\
2644 -->\n\
2645 \n\
2646 <!--\n \
2647 second skipped region\n \
2648 also skipped\n\
2649 -->\n\
2650 \n \
2651 real indented code line\n";
2652
2653 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2654 let result = rule.check(&ctx).unwrap();
2655
2656 assert_eq!(
2657 result,
2658 vec![],
2659 "Skipped container lines must not outweigh the single real indented block",
2660 );
2661 }
2662
2663 #[test]
2664 fn test_consistent_style_fenced_wins_over_skipped_indented() {
2665 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2669 let content = "# Heading\n\
2670 \n\
2671 <!--\n \
2672 skipped indented region one\n \
2673 more of region one\n\
2674 -->\n\
2675 \n\
2676 <!--\n \
2677 skipped indented region two\n \
2678 more of region two\n\
2679 -->\n\
2680 \n\
2681 ```md\n\
2682 real fenced block\n\
2683 ```\n";
2684
2685 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2686 let result = rule.check(&ctx).unwrap();
2687
2688 assert_eq!(
2689 result,
2690 vec![],
2691 "Fenced block must win when all indented lines are inside skipped containers",
2692 );
2693 }
2694
2695 #[test]
2696 fn test_four_space_indented_fence_is_not_valid_fence() {
2697 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2700
2701 assert!(rule.is_fenced_code_block_start("```"));
2703 assert!(rule.is_fenced_code_block_start(" ```"));
2704 assert!(rule.is_fenced_code_block_start(" ```"));
2705 assert!(rule.is_fenced_code_block_start(" ```"));
2706
2707 assert!(!rule.is_fenced_code_block_start(" ```"));
2709 assert!(!rule.is_fenced_code_block_start(" ```"));
2710 assert!(!rule.is_fenced_code_block_start(" ```"));
2711
2712 assert!(!rule.is_fenced_code_block_start("\t```"));
2714 }
2715
2716 #[test]
2717 fn test_issue_237_indented_fenced_block_detected_as_indented() {
2718 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2724
2725 let content = r#"## Test
2727
2728 ```js
2729 var foo = "hello";
2730 ```
2731"#;
2732
2733 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2734 let result = rule.check(&ctx).unwrap();
2735
2736 assert_eq!(
2738 result.len(),
2739 1,
2740 "4-space indented fence should be detected as indented code block"
2741 );
2742 assert!(
2743 result[0].message.contains("Use fenced code blocks"),
2744 "Expected 'Use fenced code blocks' message"
2745 );
2746 }
2747
2748 #[test]
2749 fn test_issue_276_indented_code_in_list() {
2750 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2753
2754 let content = r#"1. First item
27552. Second item with code:
2756
2757 # This is a code block in a list
2758 print("Hello, world!")
2759
27604. Third item"#;
2761
2762 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2763 let result = rule.check(&ctx).unwrap();
2764
2765 assert!(
2767 !result.is_empty(),
2768 "Indented code block inside list should be flagged when style=fenced"
2769 );
2770 assert!(
2771 result[0].message.contains("Use fenced code blocks"),
2772 "Expected 'Use fenced code blocks' message"
2773 );
2774 }
2775
2776 #[test]
2777 fn test_three_space_indented_fence_is_valid() {
2778 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2780
2781 let content = r#"## Test
2782
2783 ```js
2784 var foo = "hello";
2785 ```
2786"#;
2787
2788 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2789 let result = rule.check(&ctx).unwrap();
2790
2791 assert_eq!(
2793 result.len(),
2794 0,
2795 "3-space indented fence should be recognized as valid fenced code block"
2796 );
2797 }
2798
2799 #[test]
2800 fn test_indented_style_with_deeply_indented_fenced() {
2801 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2804
2805 let content = r#"Text
2806
2807 ```js
2808 var foo = "hello";
2809 ```
2810
2811More text
2812"#;
2813
2814 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2815 let result = rule.check(&ctx).unwrap();
2816
2817 assert_eq!(
2820 result.len(),
2821 0,
2822 "4-space indented content should be valid when style=indented"
2823 );
2824 }
2825
2826 #[test]
2827 fn test_fix_misplaced_fenced_block() {
2828 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2831
2832 let content = r#"## Test
2833
2834 ```js
2835 var foo = "hello";
2836 ```
2837"#;
2838
2839 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2840 let fixed = rule.fix(&ctx).unwrap();
2841
2842 let expected = r#"## Test
2844
2845```js
2846var foo = "hello";
2847```
2848"#;
2849
2850 assert_eq!(fixed, expected, "Fix should remove indentation, not add more fences");
2851 }
2852
2853 #[test]
2854 fn test_fix_regular_indented_block() {
2855 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2858
2859 let content = r#"Text
2860
2861 var foo = "hello";
2862 console.log(foo);
2863
2864More text
2865"#;
2866
2867 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2868 let fixed = rule.fix(&ctx).unwrap();
2869
2870 assert!(fixed.contains("```\nvar foo"), "Should add opening fence");
2872 assert!(fixed.contains("console.log(foo);\n```"), "Should add closing fence");
2873 }
2874
2875 #[test]
2876 fn test_fix_indented_block_with_fence_like_content() {
2877 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2881
2882 let content = r#"Text
2883
2884 some code
2885 ```not a fence opener
2886 more code
2887"#;
2888
2889 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2890 let fixed = rule.fix(&ctx).unwrap();
2891
2892 assert!(fixed.contains(" some code"), "Unsafe block should be left unchanged");
2894 assert!(!fixed.contains("```\nsome code"), "Should NOT wrap unsafe block");
2895 }
2896
2897 #[test]
2898 fn test_fix_mixed_indented_and_misplaced_blocks() {
2899 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2901
2902 let content = r#"Text
2903
2904 regular indented code
2905
2906More text
2907
2908 ```python
2909 print("hello")
2910 ```
2911"#;
2912
2913 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2914 let fixed = rule.fix(&ctx).unwrap();
2915
2916 assert!(
2918 fixed.contains("```\nregular indented code\n```"),
2919 "First block should be wrapped in fences"
2920 );
2921
2922 assert!(
2924 fixed.contains("\n```python\nprint(\"hello\")\n```"),
2925 "Second block should be dedented, not double-wrapped"
2926 );
2927 assert!(
2929 !fixed.contains("```\n```python"),
2930 "Should not have nested fence openers"
2931 );
2932 }
2933}