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