1use crate::rule::{Fix, FixCapability, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::rule_config_serde::{FlavorOverrideNotice, option_is_explicit};
3use crate::utils::calculate_indentation_width_default;
4use crate::utils::mdg;
5use crate::utils::mkdocs_admonitions;
6use crate::utils::mkdocs_tabs;
7use crate::utils::range_utils::calculate_line_range;
8use toml;
9
10mod md046_config;
11pub use md046_config::CodeBlockStyle;
12use md046_config::MD046Config;
13
14static MDG_STYLE_OVERRIDE: FlavorOverrideNotice = FlavorOverrideNotice::new();
16
17struct IndentContext<'a> {
19 in_list_context: &'a [bool],
20 in_tab_context: &'a [bool],
21 in_admonition_context: &'a [bool],
22 in_comment_or_html: &'a [bool],
32 list_item_baseline: &'a [Option<usize>],
42}
43
44struct OwnedIndentContext {
47 in_list_context: Vec<bool>,
48 in_tab_context: Vec<bool>,
49 in_admonition_context: Vec<bool>,
50 in_comment_or_html: Vec<bool>,
51 list_item_baseline: Vec<Option<usize>>,
52}
53
54impl OwnedIndentContext {
55 fn borrow(&self) -> IndentContext<'_> {
56 IndentContext {
57 in_list_context: &self.in_list_context,
58 in_tab_context: &self.in_tab_context,
59 in_admonition_context: &self.in_admonition_context,
60 in_comment_or_html: &self.in_comment_or_html,
61 list_item_baseline: &self.list_item_baseline,
62 }
63 }
64}
65
66#[derive(Clone)]
72pub struct MD046CodeBlockStyle {
73 config: MD046Config,
74 style_explicit: bool,
78}
79
80impl MD046CodeBlockStyle {
81 const FENCE: &'static str = "```";
83
84 pub fn new(style: CodeBlockStyle) -> Self {
85 Self {
86 config: MD046Config { style },
87 style_explicit: true,
88 }
89 }
90
91 pub fn from_config_struct(config: MD046Config) -> Self {
92 Self {
93 config,
94 style_explicit: false,
95 }
96 }
97
98 fn has_valid_fence_indent(line: &str) -> bool {
103 calculate_indentation_width_default(line) < 4
104 }
105
106 fn has_valid_fence_indent_at(line: &str, baseline: usize) -> bool {
110 let indent = calculate_indentation_width_default(line);
111 indent >= baseline && indent - baseline < 4
112 }
113
114 fn is_fenced_code_block_start(&self, line: &str) -> bool {
123 if !Self::has_valid_fence_indent(line) {
124 return false;
125 }
126
127 let trimmed = line.trim_start();
128 trimmed.starts_with("```") || trimmed.starts_with("~~~")
129 }
130
131 fn is_fenced_code_block_start_at(&self, line: &str, baseline: usize) -> bool {
132 if baseline == 0 {
133 return self.is_fenced_code_block_start(line);
134 }
135
136 Self::has_valid_fence_indent_at(line, baseline)
137 && (line.trim_start().starts_with("```") || line.trim_start().starts_with("~~~"))
138 }
139
140 fn is_closing_fence(line: &str, fence_char: char, opener_len: usize, baseline: usize) -> bool {
141 if !Self::has_valid_fence_indent_at(line, baseline) {
142 return false;
143 }
144
145 let trimmed = line.trim_start();
146 let closer_len = trimmed.chars().take_while(|&ch| ch == fence_char).count();
147 closer_len >= opener_len && closer_len > 0 && trimmed[closer_len..].trim().is_empty()
148 }
149
150 fn strip_indentation_columns(line: &str, columns: usize) -> String {
155 if columns == 0 {
156 return line.to_string();
157 }
158
159 let mut width = 0usize;
160 let mut consumed = 0usize;
161
162 for (byte_index, ch) in line.char_indices() {
163 let next_width = match ch {
164 ' ' => width + 1,
165 '\t' => ((width / 4) + 1) * 4,
166 _ => break,
167 };
168 consumed = byte_index + ch.len_utf8();
169
170 if next_width >= columns {
171 let remainder = next_width - columns;
172 let mut stripped = String::with_capacity(remainder + line.len() - consumed);
173 stripped.extend(std::iter::repeat_n(' ', remainder));
174 stripped.push_str(&line[consumed..]);
175 return stripped;
176 }
177
178 width = next_width;
179 }
180
181 line[consumed..].to_string()
182 }
183
184 fn is_list_item(&self, line: &str) -> bool {
185 let trimmed = line.trim_start();
186 if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ") {
187 return true;
188 }
189 let after_digits = trimmed.trim_start_matches(|c: char| c.is_ascii_digit());
194 after_digits.len() < trimmed.len() && (after_digits.starts_with(". ") || after_digits.starts_with(") "))
195 }
196
197 fn is_footnote_definition(&self, line: &str) -> bool {
217 let trimmed = line.trim_start();
218 if !trimmed.starts_with("[^") || trimmed.len() < 5 {
219 return false;
220 }
221
222 if let Some(close_bracket_pos) = trimmed.find("]:")
223 && close_bracket_pos > 2
224 {
225 let label = &trimmed[2..close_bracket_pos];
226
227 if label.trim().is_empty() {
228 return false;
229 }
230
231 if label.contains('\r') {
233 return false;
234 }
235
236 if label.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
238 return true;
239 }
240 }
241
242 false
243 }
244
245 fn precompute_block_continuation_context(&self, lines: &[&str]) -> Vec<bool> {
268 let mut in_continuation_context = vec![false; lines.len()];
269 let mut last_list_item_line: Option<usize> = None;
270 let mut last_footnote_line: Option<usize> = None;
271 let mut blank_line_count = 0;
272
273 for (i, line) in lines.iter().enumerate() {
274 let trimmed = line.trim_start();
275 let indent_len = line.len() - trimmed.len();
276
277 if self.is_list_item(line) {
279 last_list_item_line = Some(i);
280 last_footnote_line = None; blank_line_count = 0;
282 in_continuation_context[i] = true;
283 continue;
284 }
285
286 if self.is_footnote_definition(line) {
288 last_footnote_line = Some(i);
289 last_list_item_line = None; blank_line_count = 0;
291 in_continuation_context[i] = true;
292 continue;
293 }
294
295 if line.trim().is_empty() {
297 if last_list_item_line.is_some() || last_footnote_line.is_some() {
299 blank_line_count += 1;
300 in_continuation_context[i] = true;
301
302 }
306 continue;
307 }
308
309 if indent_len == 0 && !trimmed.is_empty() {
311 if trimmed.starts_with('#') {
315 last_list_item_line = None;
316 last_footnote_line = None;
317 blank_line_count = 0;
318 continue;
319 }
320
321 if trimmed.starts_with("---") || trimmed.starts_with("***") {
323 last_list_item_line = None;
324 last_footnote_line = None;
325 blank_line_count = 0;
326 continue;
327 }
328
329 if let Some(list_line) = last_list_item_line
332 && (i - list_line > 5 || blank_line_count > 1)
333 {
334 last_list_item_line = None;
335 }
336
337 if last_footnote_line.is_some() {
339 last_footnote_line = None;
340 }
341
342 blank_line_count = 0;
343
344 if last_list_item_line.is_none() && last_footnote_line.is_some() {
346 last_footnote_line = None;
347 }
348 continue;
349 }
350
351 if indent_len > 0 && (last_list_item_line.is_some() || last_footnote_line.is_some()) {
353 in_continuation_context[i] = true;
354 blank_line_count = 0;
355 }
356 }
357
358 in_continuation_context
359 }
360
361 fn precompute_list_item_baseline(
372 &self,
373 ctx: &crate::lint_context::LintContext,
374 lines: &[&str],
375 ) -> Vec<Option<usize>> {
376 let mut baselines = vec![None; lines.len()];
377 let mut last_baseline: Option<usize> = None;
378 let mut last_list_item_line: Option<usize> = None;
379 let mut blank_line_count = 0usize;
380 let mut last_item_quoted = false;
381
382 for (i, line) in lines.iter().enumerate() {
383 let trimmed = line.trim_start();
384 let indent_len = line.len() - trimmed.len();
385
386 if let Some(info) = ctx.line_info(i + 1)
388 && let Some(item) = info.list_item.as_ref()
389 {
390 last_baseline = Some(item.content_column);
391 last_list_item_line = Some(i);
392 last_item_quoted = info.blockquote.is_some();
393 blank_line_count = 0;
394 baselines[i] = last_baseline;
395 continue;
396 }
397
398 if last_item_quoted && line.trim().is_empty() {
401 last_baseline = None;
402 last_list_item_line = None;
403 last_item_quoted = false;
404 continue;
405 }
406
407 if line.trim().is_empty() {
409 if last_baseline.is_some() {
410 blank_line_count += 1;
411 baselines[i] = last_baseline;
412 }
413 continue;
414 }
415
416 if indent_len == 0 {
420 if trimmed.starts_with('#') || trimmed.starts_with("---") || trimmed.starts_with("***") {
421 last_baseline = None;
422 last_list_item_line = None;
423 } else if let Some(list_line) = last_list_item_line
424 && (i - list_line > 5 || blank_line_count > 1)
425 {
426 last_baseline = None;
427 last_list_item_line = None;
428 }
429 blank_line_count = 0;
430 continue;
431 }
432
433 if last_baseline.is_some() {
435 baselines[i] = last_baseline;
436 blank_line_count = 0;
437 }
438 }
439
440 baselines
441 }
442
443 fn is_indented_code_block_with_context(
447 &self,
448 lines: &[&str],
449 i: usize,
450 is_mkdocs: bool,
451 ctx: &IndentContext,
452 prev_is_code: bool,
453 ) -> bool {
454 if i >= lines.len() {
455 return false;
456 }
457
458 let line = lines[i];
459
460 if line.trim().is_empty() {
465 return false;
466 }
467
468 let indent = calculate_indentation_width_default(line);
470 if indent < 4 {
471 return false;
472 }
473
474 if ctx.in_list_context[i] {
480 let crosses_baseline = ctx
481 .list_item_baseline
482 .get(i)
483 .copied()
484 .flatten()
485 .is_some_and(|base| indent >= base + 4);
486 if !crosses_baseline {
487 return false;
488 }
489 }
490
491 if is_mkdocs && ctx.in_tab_context[i] {
493 return false;
494 }
495
496 if is_mkdocs && ctx.in_admonition_context[i] {
499 return false;
500 }
501
502 if ctx.in_comment_or_html.get(i).copied().unwrap_or(false) {
508 return false;
509 }
510
511 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
519 has_blank_line_before || prev_is_code
520 }
521
522 fn first_code_block_line(
531 ctx: &crate::lint_context::LintContext,
532 block_lines: &[bool],
533 start: usize,
534 block_end: usize,
535 ) -> Option<usize> {
536 (start..block_lines.len())
537 .take_while(|&idx| ctx.line_offsets.get(idx).is_some_and(|&offset| offset < block_end))
538 .find(|&idx| block_lines[idx])
539 }
540
541 fn indented_block_lines(
557 &self,
558 lines: &[&str],
559 is_mkdocs: bool,
560 ictx: &IndentContext<'_>,
561 ctx: &crate::lint_context::LintContext,
562 ) -> Vec<bool> {
563 let mut member = vec![false; lines.len()];
564 for i in 0..lines.len() {
565 let prev_is_code = i > 0 && member[i - 1];
566 member[i] = self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx, prev_is_code);
567 }
568
569 if ctx.flavor == crate::config::MarkdownFlavor::MDG {
576 let mut i = 0;
577 while i < member.len() {
578 if !member[i] {
579 i += 1;
580 continue;
581 }
582 let start = i;
583 while i < member.len() && member[i] {
584 i += 1;
585 }
586 if lines[start..i].iter().all(|line| mdg::is_table_row(line)) {
587 member[start..i].fill(false);
588 }
589 }
590 }
591
592 let mut i = 0;
593 while i < lines.len() {
594 if !member[i] {
595 i += 1;
596 continue;
597 }
598 let mut next = i + 1;
599 while next < lines.len() && lines[next].trim().is_empty() {
600 next += 1;
601 }
602 if next < lines.len() && member[next] {
603 member[i + 1..next].fill(true);
604 }
605 i = next;
606 }
607
608 member
609 }
610
611 fn precompute_comment_or_html_context(ctx: &crate::lint_context::LintContext, line_count: usize) -> Vec<bool> {
620 (0..line_count)
621 .map(|i| {
622 ctx.line_info(i + 1).is_some_and(|info| {
623 info.in_html_comment
624 || info.in_mdx_comment
625 || info.in_html_block
626 || info.in_jsx_block
627 || info.in_mkdocstrings
628 || info.in_footnote_definition
629 || info.blockquote.is_some()
630 || info.in_front_matter
631 })
632 })
633 .collect()
634 }
635
636 fn precompute_mkdocs_tab_context(&self, lines: &[&str]) -> Vec<bool> {
638 let mut in_tab_context = vec![false; lines.len()];
639 let mut current_tab_indent: Option<usize> = None;
640
641 for (i, line) in lines.iter().enumerate() {
642 if mkdocs_tabs::is_tab_marker(line) {
644 let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
645 current_tab_indent = Some(tab_indent);
646 in_tab_context[i] = true;
647 continue;
648 }
649
650 if let Some(tab_indent) = current_tab_indent {
652 if mkdocs_tabs::is_tab_content(line, tab_indent) {
653 in_tab_context[i] = true;
654 } else if !line.trim().is_empty() && calculate_indentation_width_default(line) < 4 {
655 current_tab_indent = None;
657 } else {
658 in_tab_context[i] = true;
660 }
661 }
662 }
663
664 in_tab_context
665 }
666
667 fn precompute_mkdocs_admonition_context(&self, lines: &[&str]) -> Vec<bool> {
676 let mut in_admonition_context = vec![false; lines.len()];
677 let mut admonition_stack: Vec<usize> = Vec::new();
679
680 for (i, line) in lines.iter().enumerate() {
681 let line_indent = calculate_indentation_width_default(line);
682
683 if mkdocs_admonitions::is_admonition_start(line) {
685 let adm_indent = mkdocs_admonitions::get_admonition_indent(line).unwrap_or(0);
686
687 while let Some(&top_indent) = admonition_stack.last() {
689 if adm_indent <= top_indent {
691 admonition_stack.pop();
692 } else {
693 break;
694 }
695 }
696
697 admonition_stack.push(adm_indent);
699 in_admonition_context[i] = true;
700 continue;
701 }
702
703 if line.trim().is_empty() {
705 if !admonition_stack.is_empty() {
706 in_admonition_context[i] = true;
707 }
708 continue;
709 }
710
711 while let Some(&top_indent) = admonition_stack.last() {
714 if line_indent >= top_indent + 4 {
716 break;
718 } else {
719 admonition_stack.pop();
721 }
722 }
723
724 if !admonition_stack.is_empty() {
726 in_admonition_context[i] = true;
727 }
728 }
729
730 in_admonition_context
731 }
732
733 fn build_indent_context(
745 &self,
746 ctx: &crate::lint_context::LintContext,
747 lines: &[&str],
748 is_mkdocs: bool,
749 ) -> OwnedIndentContext {
750 OwnedIndentContext {
751 in_list_context: self.precompute_block_continuation_context(lines),
752 in_tab_context: if is_mkdocs {
753 self.precompute_mkdocs_tab_context(lines)
754 } else {
755 vec![false; lines.len()]
756 },
757 in_admonition_context: if is_mkdocs {
758 self.precompute_mkdocs_admonition_context(lines)
759 } else {
760 vec![false; lines.len()]
761 },
762 in_comment_or_html: Self::precompute_comment_or_html_context(ctx, lines.len()),
763 list_item_baseline: self.precompute_list_item_baseline(ctx, lines),
764 }
765 }
766
767 fn categorize_indented_blocks(&self, lines: &[&str], block_lines: &[bool]) -> (Vec<bool>, Vec<bool>) {
779 let mut is_misplaced = vec![false; lines.len()];
780 let mut contains_fences = vec![false; lines.len()];
781
782 let mut i = 0;
784 while i < lines.len() {
785 if !block_lines[i] {
787 i += 1;
788 continue;
789 }
790
791 let block_start = i;
793 let mut block_end = i;
794
795 while block_end < lines.len() && block_lines[block_end] {
796 block_end += 1;
797 }
798
799 if block_end > block_start {
801 let first_line = lines[block_start].trim_start();
802 let last_line = lines[block_end - 1].trim_start();
803
804 let is_backtick_fence = first_line.starts_with("```");
806 let is_tilde_fence = first_line.starts_with("~~~");
807
808 if is_backtick_fence || is_tilde_fence {
809 let fence_char = if is_backtick_fence { '`' } else { '~' };
810 let opener_len = first_line.chars().take_while(|&c| c == fence_char).count();
811
812 let closer_fence_len = last_line.chars().take_while(|&c| c == fence_char).count();
814 let after_closer = &last_line[closer_fence_len..];
815
816 if closer_fence_len >= opener_len && after_closer.trim().is_empty() {
817 is_misplaced[block_start..block_end].fill(true);
819 } else {
820 contains_fences[block_start..block_end].fill(true);
822 }
823 } else {
824 let has_fence_markers = (block_start..block_end).any(|j| {
827 let trimmed = lines[j].trim_start();
828 trimmed.starts_with("```") || trimmed.starts_with("~~~")
829 });
830
831 if has_fence_markers {
832 contains_fences[block_start..block_end].fill(true);
833 }
834 }
835 }
836
837 i = block_end;
838 }
839
840 (is_misplaced, contains_fences)
841 }
842
843 fn check_unclosed_code_blocks(&self, ctx: &crate::lint_context::LintContext) -> Vec<LintWarning> {
844 let mut warnings = Vec::new();
845 let lines = ctx.raw_lines();
846
847 let has_markdown_doc_block = ctx.code_block_details.iter().any(|d| {
849 if !d.is_fenced {
850 return false;
851 }
852 let lang = d.info_string.to_lowercase();
853 lang.starts_with("markdown") || lang.starts_with("md")
854 });
855
856 if has_markdown_doc_block {
859 return warnings;
860 }
861
862 for detail in &ctx.code_block_details {
863 if !detail.is_fenced {
864 continue;
865 }
866
867 if detail.end != ctx.content.len() {
869 continue;
870 }
871
872 let opening_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
874 Ok(idx) => idx,
875 Err(idx) => idx.saturating_sub(1),
876 };
877
878 let line = lines.get(opening_line_idx).unwrap_or(&"");
879 let fence_pos = line.find("```").into_iter().chain(line.find("~~~")).min().unwrap_or(0);
880 let fence_char = line[fence_pos..].chars().next().unwrap_or('`');
881 let fence_marker: String = line[fence_pos..].chars().take_while(|&ch| ch == fence_char).collect();
882 let opening_quote = crate::utils::blockquote::parse_blockquote_prefix(line);
883 let quote_level = opening_quote.map_or(0, |quote| quote.nesting_level);
884 let owned = self.build_indent_context(ctx, lines, ctx.flavor == crate::config::MarkdownFlavor::MkDocs);
885 let baseline = owned
886 .list_item_baseline
887 .get(opening_line_idx)
888 .copied()
889 .flatten()
890 .unwrap_or(0);
891
892 let has_closing_fence = lines
895 .iter()
896 .enumerate()
897 .rev()
898 .find_map(|(idx, candidate)| {
899 let quote = crate::utils::blockquote::parse_blockquote_prefix(candidate);
900 let body = quote.map_or(*candidate, |quote| quote.content);
901 if body.trim().is_empty() {
902 return None;
903 }
904 Some(
905 idx > opening_line_idx
906 && quote.map_or(0, |quote| quote.nesting_level) == quote_level
907 && Self::is_closing_fence(body, fence_char, fence_marker.len(), baseline),
908 )
909 })
910 .unwrap_or(false);
911
912 if !has_closing_fence {
913 if ctx
915 .lines
916 .get(opening_line_idx)
917 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
918 {
919 continue;
920 }
921
922 let (start_line, start_col, end_line, end_col) = calculate_line_range(opening_line_idx + 1, line);
923
924 warnings.push(LintWarning {
925 rule_name: Some(self.name().to_string()),
926 line: start_line,
927 column: start_col,
928 end_line,
929 end_column: end_col,
930 message: format!("Code block opened with '{fence_marker}' but never closed"),
931 severity: Severity::Warning,
932 fix: Some(Fix::new(ctx.content.len()..ctx.content.len(), {
933 let prefix: String = line[..fence_pos]
936 .chars()
937 .map(|ch| if ch == '>' || ch.is_whitespace() { ch } else { ' ' })
938 .collect();
939 let newline = crate::utils::detect_line_ending(ctx.content);
940 if ctx.content.ends_with('\n') {
941 format!("{prefix}{fence_marker}{newline}")
942 } else {
943 format!("{newline}{prefix}{fence_marker}")
944 }
945 })),
946 });
947 }
948 }
949
950 warnings
951 }
952
953 fn effective_target_style(
961 &self,
962 ctx: &crate::lint_context::LintContext,
963 detect: impl FnOnce() -> CodeBlockStyle,
964 ) -> CodeBlockStyle {
965 if ctx.flavor == crate::config::MarkdownFlavor::MDG {
966 self.warn_once_about_overridden_style();
967 return CodeBlockStyle::Fenced;
968 }
969
970 match self.config.style {
971 CodeBlockStyle::Consistent => {
972 let detected = detect();
973 if detected == CodeBlockStyle::Indented
974 && ctx.code_block_details.iter().any(|detail| {
975 detail.is_fenced
976 && !detail.info_string.trim().is_empty()
977 && Self::code_block_is_style_eligible(ctx, detail)
978 })
979 {
980 CodeBlockStyle::Fenced
984 } else {
985 detected
986 }
987 }
988 style => style,
989 }
990 }
991
992 fn code_block_is_style_eligible(
996 ctx: &crate::lint_context::LintContext,
997 detail: &crate::utils::code_block_utils::CodeBlockDetail,
998 ) -> bool {
999 let Some(line_idx) = Self::code_block_start_line(ctx, detail) else {
1000 return false;
1001 };
1002
1003 !ctx.lines.get(line_idx).is_some_and(|info| {
1004 info.in_html_comment
1005 || info.in_mdx_comment
1006 || info.in_html_block
1007 || info.in_jsx_block
1008 || info.in_mkdocstrings
1009 || info.in_footnote_definition
1010 || info.blockquote.is_some()
1011 || info.in_front_matter
1012 })
1013 }
1014
1015 fn code_block_start_line(
1016 ctx: &crate::lint_context::LintContext,
1017 detail: &crate::utils::code_block_utils::CodeBlockDetail,
1018 ) -> Option<usize> {
1019 if detail.start >= ctx.content.len() {
1020 return None;
1021 }
1022
1023 Some(match ctx.line_offsets.binary_search(&detail.start) {
1024 Ok(idx) => idx,
1025 Err(idx) => idx.saturating_sub(1),
1026 })
1027 }
1028
1029 fn fenced_separator_lines(ctx: &crate::lint_context::LintContext) -> std::collections::HashSet<usize> {
1033 let mut lines = std::collections::HashSet::new();
1034
1035 for pair in ctx.code_block_details.windows(2) {
1036 let [previous, next] = pair else {
1037 continue;
1038 };
1039 if previous.end > next.start || next.start > ctx.content.len() {
1040 continue;
1041 }
1042 if !ctx.content[previous.end..next.start].trim().is_empty() {
1043 continue;
1044 }
1045
1046 for detail in [previous, next] {
1047 if detail.is_fenced
1048 && let Some(line) = Self::code_block_start_line(ctx, detail)
1049 {
1050 lines.insert(line);
1051 }
1052 }
1053 }
1054
1055 lines
1056 }
1057
1058 fn fenced_boundary_blank_lines(
1063 ctx: &crate::lint_context::LintContext,
1064 lines: &[&str],
1065 ictx: &IndentContext,
1066 ) -> std::collections::HashSet<usize> {
1067 let mut boundary_blank_lines = std::collections::HashSet::new();
1068
1069 for detail in ctx.code_block_details.iter().filter(|detail| detail.is_fenced) {
1070 let Some(start) = Self::code_block_start_line(ctx, detail) else {
1071 continue;
1072 };
1073 let Some(opener) = lines.get(start) else {
1074 continue;
1075 };
1076 let baseline = ictx.list_item_baseline.get(start).copied().flatten().unwrap_or(0);
1077 let trimmed = opener.trim_start();
1078 if !Self::has_valid_fence_indent_at(opener, baseline) {
1079 continue;
1080 }
1081 let fence_char = if trimmed.starts_with("```") {
1082 '`'
1083 } else if trimmed.starts_with("~~~") {
1084 '~'
1085 } else {
1086 continue;
1089 };
1090 let opener_len = trimmed.chars().take_while(|&ch| ch == fence_char).count();
1091
1092 let mut block_end = start + 1;
1093 let mut closer = None;
1094 while block_end < lines.len()
1095 && ctx
1096 .line_offsets
1097 .get(block_end)
1098 .is_some_and(|&offset| offset < detail.end)
1099 {
1100 if Self::is_closing_fence(lines[block_end], fence_char, opener_len, baseline) {
1101 closer = Some(block_end);
1102 break;
1103 }
1104 block_end += 1;
1105 }
1106
1107 let payload_end = closer.unwrap_or(block_end);
1108 if start + 1 == payload_end
1109 || (start + 1 < payload_end
1110 && (lines[start + 1].trim().is_empty() || lines[payload_end - 1].trim().is_empty()))
1111 {
1112 boundary_blank_lines.insert(start);
1113 }
1114 }
1115
1116 boundary_blank_lines
1117 }
1118
1119 fn warn_once_about_overridden_style(&self) {
1125 if !self.style_explicit || self.config.style != CodeBlockStyle::Indented {
1126 return;
1127 }
1128
1129 MDG_STYLE_OVERRIDE.report(
1130 "MD046",
1131 "style",
1132 "indented",
1133 "fenced",
1134 "a Gherkin Doc String is only ever a backtick fence",
1135 );
1136 }
1137
1138 fn detect_style(
1139 &self,
1140 ctx: &crate::lint_context::LintContext,
1141 lines: &[&str],
1142 is_mkdocs: bool,
1143 ictx: &IndentContext,
1144 ) -> Option<CodeBlockStyle> {
1145 if lines.is_empty() {
1146 return None;
1147 }
1148
1149 let block_lines = self.indented_block_lines(lines, is_mkdocs, ictx, ctx);
1150
1151 let mut fenced_count = 0;
1152 let mut indented_count = 0;
1153
1154 let mut in_fenced = false;
1164 let mut prev_was_indented = false;
1165
1166 for (i, line) in lines.iter().enumerate() {
1167 let in_container = ictx.in_comment_or_html.get(i).copied().unwrap_or(false);
1168
1169 if ctx.flavor.supports_colon_code_fences() && ctx.lines.get(i).is_some_and(|l| l.in_code_block) {
1173 prev_was_indented = false;
1174 continue;
1175 }
1176
1177 if ctx.flavor.supports_myst_directives() && ctx.lines.get(i).is_some_and(|l| l.in_myst_directive) {
1179 prev_was_indented = false;
1180 continue;
1181 }
1182
1183 let baseline = ictx.list_item_baseline.get(i).copied().flatten().unwrap_or(0);
1184 if self.is_fenced_code_block_start_at(line, baseline) {
1185 if in_container {
1186 prev_was_indented = false;
1189 continue;
1190 }
1191 if !in_fenced {
1192 fenced_count += 1;
1194 in_fenced = true;
1195 } else {
1196 in_fenced = false;
1198 }
1199 prev_was_indented = false;
1200 } else if !in_fenced && block_lines[i] {
1201 if !prev_was_indented {
1203 indented_count += 1;
1204 }
1205 prev_was_indented = true;
1206 } else {
1207 prev_was_indented = false;
1208 }
1209 }
1210
1211 if fenced_count == 0 && indented_count == 0 {
1212 None
1213 } else if fenced_count > 0 && indented_count == 0 {
1214 Some(CodeBlockStyle::Fenced)
1215 } else if fenced_count == 0 && indented_count > 0 {
1216 Some(CodeBlockStyle::Indented)
1217 } else if fenced_count >= indented_count {
1218 Some(CodeBlockStyle::Fenced)
1219 } else {
1220 Some(CodeBlockStyle::Indented)
1221 }
1222 }
1223}
1224
1225impl Rule for MD046CodeBlockStyle {
1226 fn name(&self) -> &'static str {
1227 "MD046"
1228 }
1229
1230 fn description(&self) -> &'static str {
1231 "Code blocks should use a consistent style"
1232 }
1233
1234 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
1235 if ctx.content.is_empty() {
1237 return Ok(Vec::new());
1238 }
1239
1240 if !ctx.content.contains("```")
1242 && !ctx.content.contains("~~~")
1243 && !ctx.content.contains(" ")
1244 && !ctx.content.contains('\t')
1245 {
1246 return Ok(Vec::new());
1247 }
1248
1249 let mut unclosed_warnings = self.check_unclosed_code_blocks(ctx);
1251
1252 if !unclosed_warnings.is_empty() {
1254 let fixed = self.fix(ctx)?;
1255 for warning in &mut unclosed_warnings {
1256 warning.fix = if fixed == ctx.content {
1257 None
1258 } else if let Some(suffix) = fixed.strip_prefix(ctx.content) {
1259 Some(Fix::new(ctx.content.len()..ctx.content.len(), suffix.to_string()))
1260 } else {
1261 Some(Fix::new(0..ctx.content.len(), fixed.clone()))
1262 };
1263 }
1264 return Ok(unclosed_warnings);
1265 }
1266
1267 let lines = ctx.raw_lines();
1269 let mut warnings = Vec::new();
1270
1271 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
1272
1273 let target_style = self.effective_target_style(ctx, || {
1275 let owned = self.build_indent_context(ctx, lines, is_mkdocs);
1276 let detected = self.detect_style(ctx, lines, is_mkdocs, &owned.borrow());
1277 detected.unwrap_or(CodeBlockStyle::Fenced)
1278 });
1279
1280 let mdg_block_lines = (ctx.flavor == crate::config::MarkdownFlavor::MDG
1285 && ctx.code_block_details.iter().any(|detail| !detail.is_fenced))
1286 .then(|| {
1287 let owned = self.build_indent_context(ctx, lines, is_mkdocs);
1288 self.indented_block_lines(lines, is_mkdocs, &owned.borrow(), ctx)
1289 });
1290
1291 let mut reported_indented_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
1293
1294 for detail in &ctx.code_block_details {
1295 if detail.start >= ctx.content.len() || detail.end > ctx.content.len() {
1296 continue;
1297 }
1298
1299 let start_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
1300 Ok(idx) => idx,
1301 Err(idx) => idx.saturating_sub(1),
1302 };
1303
1304 if detail.is_fenced {
1305 if target_style == CodeBlockStyle::Indented {
1306 let line = lines.get(start_line_idx).unwrap_or(&"");
1307
1308 if ctx
1309 .lines
1310 .get(start_line_idx)
1311 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment || info.in_footnote_definition)
1312 {
1313 continue;
1314 }
1315
1316 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
1317 warnings.push(LintWarning {
1318 rule_name: Some(self.name().to_string()),
1319 line: start_line,
1320 column: start_col,
1321 end_line,
1322 end_column: end_col,
1323 message: "Use indented code blocks".to_string(),
1324 severity: Severity::Warning,
1325 fix: None,
1326 });
1327 }
1328 } else {
1329 if target_style == CodeBlockStyle::Fenced {
1331 let start_line_idx = match &mdg_block_lines {
1336 Some(block_lines) => {
1337 match Self::first_code_block_line(ctx, block_lines, start_line_idx, detail.end) {
1338 Some(idx) => idx,
1339 None => continue,
1340 }
1341 }
1342 None => start_line_idx,
1343 };
1344
1345 if reported_indented_lines.contains(&start_line_idx) {
1346 continue;
1347 }
1348
1349 let line = lines.get(start_line_idx).unwrap_or(&"");
1350
1351 if ctx.lines.get(start_line_idx).is_some_and(|info| {
1353 info.in_html_comment
1354 || info.in_mdx_comment
1355 || info.in_html_block
1356 || info.in_jsx_block
1357 || info.in_mkdocstrings
1358 || info.in_footnote_definition
1359 || info.blockquote.is_some()
1360 || info.in_front_matter
1361 }) {
1362 continue;
1363 }
1364
1365 if is_mkdocs
1367 && ctx
1368 .lines
1369 .get(start_line_idx)
1370 .is_some_and(|info| info.in_admonition || info.in_content_tab)
1371 {
1372 continue;
1373 }
1374
1375 reported_indented_lines.insert(start_line_idx);
1376
1377 let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
1378 warnings.push(LintWarning {
1379 rule_name: Some(self.name().to_string()),
1380 line: start_line,
1381 column: start_col,
1382 end_line,
1383 end_column: end_col,
1384 message: "Use fenced code blocks".to_string(),
1385 severity: Severity::Warning,
1386 fix: None,
1387 });
1388 }
1389 }
1390 }
1391
1392 if !warnings.is_empty() {
1395 let conversion = self.convert_closed_blocks(ctx)?;
1396 for warning in &mut warnings {
1397 let line_idx = warning.line - 1;
1398 warning.fix = conversion
1399 .edits
1400 .iter()
1401 .find(|edit| edit.lines.contains(&line_idx))
1402 .map(|edit| edit.fix.clone());
1403 }
1404 }
1405
1406 warnings.sort_by_key(|w| (w.line, w.column));
1408
1409 Ok(warnings)
1410 }
1411
1412 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
1413 let content = ctx.content;
1414 if content.is_empty() {
1415 return Ok(String::new());
1416 }
1417
1418 let unclosed = crate::utils::fix_utils::filter_warnings_by_inline_config(
1419 self.check_unclosed_code_blocks(ctx),
1420 ctx.inline_config(),
1421 self.name(),
1422 );
1423 if !unclosed.is_empty() {
1424 let repaired =
1425 crate::utils::fix_utils::apply_warning_fixes(content, &unclosed).map_err(LintError::FixFailed)?;
1426 let repaired_ctx = crate::lint_context::LintContext::new(
1427 &repaired,
1428 ctx.flavor,
1429 ctx.source_file().map(std::path::Path::to_path_buf),
1430 );
1431 return Ok(self.convert_closed_blocks(&repaired_ctx)?.content);
1434 }
1435
1436 Ok(self.convert_closed_blocks(ctx)?.content)
1437 }
1438
1439 fn category(&self) -> RuleCategory {
1441 RuleCategory::CodeBlock
1442 }
1443
1444 fn fix_capability(&self) -> FixCapability {
1445 FixCapability::ConditionallyFixable
1448 }
1449
1450 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
1452 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
1455 }
1456
1457 fn as_any(&self) -> &dyn std::any::Any {
1458 self
1459 }
1460
1461 crate::impl_rule_config_sections!(MD046Config);
1462
1463 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1464 where
1465 Self: Sized,
1466 {
1467 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
1468 let style_explicit = option_is_explicit(config, "MD046", "style");
1469
1470 Box::new(Self {
1471 config: rule_config,
1472 style_explicit,
1473 })
1474 }
1475}
1476
1477impl MD046CodeBlockStyle {
1478 fn convert_closed_blocks(&self, ctx: &crate::lint_context::LintContext) -> Result<Conversion, LintError> {
1482 let content = ctx.content;
1483 let lines = ctx.raw_lines();
1484
1485 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
1487
1488 let owned = self.build_indent_context(ctx, lines, is_mkdocs);
1489 let ictx = owned.borrow();
1490
1491 let target_style = self.effective_target_style(ctx, || {
1492 self.detect_style(ctx, lines, is_mkdocs, &ictx)
1493 .unwrap_or(CodeBlockStyle::Fenced)
1494 });
1495
1496 let block_lines = self.indented_block_lines(lines, is_mkdocs, &ictx, ctx);
1497 let fenced_separator_lines = if target_style == CodeBlockStyle::Indented {
1498 Self::fenced_separator_lines(ctx)
1499 } else {
1500 std::collections::HashSet::new()
1501 };
1502 let fenced_boundary_blank_lines = if target_style == CodeBlockStyle::Indented {
1503 Self::fenced_boundary_blank_lines(ctx, lines, &ictx)
1504 } else {
1505 std::collections::HashSet::new()
1506 };
1507 let fenced_start_lines: std::collections::HashSet<usize> = ctx
1511 .code_block_details
1512 .iter()
1513 .filter(|detail| detail.is_fenced)
1514 .filter_map(|detail| Self::code_block_start_line(ctx, detail))
1515 .collect();
1516 let has_unsupported_fence_opener = ctx
1517 .code_block_details
1518 .iter()
1519 .filter(|detail| detail.is_fenced && Self::code_block_is_style_eligible(ctx, detail))
1520 .filter_map(|detail| Self::code_block_start_line(ctx, detail))
1521 .any(|line_index| {
1522 let Some(line) = lines.get(line_index) else {
1523 return true;
1524 };
1525 let baseline = ictx.list_item_baseline.get(line_index).copied().flatten().unwrap_or(0);
1526 !self.is_fenced_code_block_start_at(line, baseline)
1527 });
1528
1529 let (misplaced_fence_lines, unsafe_fence_lines) = self.categorize_indented_blocks(lines, &block_lines);
1533
1534 let mut result = String::with_capacity(content.len());
1535 let mut in_fenced_block = false;
1536 let mut fenced_fence_opener: Option<(char, usize)> = None;
1540 let mut in_indented_block = false;
1541 let mut current_block_fence_indent = String::new();
1546
1547 let mut current_block_must_stay_fenced = false;
1551 let mut current_fence_indent = 0usize;
1552 let mut current_fence_baseline = 0usize;
1553 let mut current_block_indented_prefix = String::from(" ");
1554 let mut converted_fenced_to_indented = false;
1555 let mut retained_structurally_unsafe_fence =
1556 target_style == CodeBlockStyle::Indented && has_unsupported_fence_opener;
1557
1558 let mut output_starts = Vec::with_capacity(lines.len() + 1);
1561 let mut line_blocks: Vec<Option<usize>> = vec![None; lines.len()];
1562 let mut current_block: Option<usize> = None;
1563
1564 for (i, line) in lines.iter().enumerate() {
1565 output_starts.push(result.len());
1566 let line_num = i + 1;
1567 let trimmed = line.trim_start();
1568 let list_baseline = ictx.list_item_baseline.get(i).copied().flatten();
1569 let fence_baseline = list_baseline.unwrap_or(0);
1570
1571 if !in_fenced_block
1574 && fenced_start_lines.contains(&i)
1575 && Self::has_valid_fence_indent_at(line, fence_baseline)
1576 && (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
1577 {
1578 let block_disabled = ctx.inline_config().is_rule_disabled(self.name(), line_num);
1580 in_fenced_block = true;
1581 current_block = Some(i);
1582 line_blocks[i] = current_block;
1583 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
1584 let opener_len = trimmed.chars().take_while(|&c| c == fence_char).count();
1585 fenced_fence_opener = Some((fence_char, opener_len));
1586 current_fence_indent = calculate_indentation_width_default(line);
1587 current_fence_baseline = fence_baseline;
1588 current_block_indented_prefix = " ".repeat(fence_baseline + 4);
1589 let follows_list_item = i
1590 .checked_sub(1)
1591 .and_then(|previous| ictx.list_item_baseline.get(previous))
1592 .copied()
1593 .flatten()
1594 .is_some();
1595 let would_become_list_prose = target_style == CodeBlockStyle::Indented
1596 && list_baseline.is_none()
1597 && (ictx.in_list_context.get(i).copied().unwrap_or(false) || follows_list_item);
1598 let would_interrupt_paragraph = target_style == CodeBlockStyle::Indented
1599 && i > 0
1600 && !lines[i - 1].trim().is_empty()
1601 && ctx
1602 .lines
1603 .get(i - 1)
1604 .is_some_and(crate::lint_context::LineInfo::is_paragraph_context)
1605 && crate::lint_context::is_paragraph_text_line(lines[i - 1]);
1606 let would_merge_code_blocks = fenced_separator_lines.contains(&i);
1607 let would_lose_boundary_blanks = fenced_boundary_blank_lines.contains(&i);
1608 current_block_must_stay_fenced = block_disabled
1609 || !trimmed[opener_len..].trim().is_empty()
1610 || would_become_list_prose
1611 || would_interrupt_paragraph
1612 || would_merge_code_blocks
1613 || would_lose_boundary_blanks;
1614 retained_structurally_unsafe_fence |= would_become_list_prose
1615 || would_interrupt_paragraph
1616 || would_merge_code_blocks
1617 || would_lose_boundary_blanks;
1618
1619 if current_block_must_stay_fenced {
1620 result.push_str(line);
1623 result.push('\n');
1624 } else if target_style == CodeBlockStyle::Indented {
1625 in_indented_block = true;
1627 converted_fenced_to_indented = true;
1628 } else {
1629 result.push_str(line);
1631 result.push('\n');
1632 }
1633 } else if in_fenced_block && fenced_fence_opener.is_some() {
1634 line_blocks[i] = current_block;
1635 let (fence_char, opener_len) = fenced_fence_opener.unwrap();
1636 let is_closer = Self::is_closing_fence(line, fence_char, opener_len, current_fence_baseline);
1639 if is_closer {
1640 in_fenced_block = false;
1641 fenced_fence_opener = None;
1642 in_indented_block = false;
1643
1644 if current_block_must_stay_fenced {
1645 result.push_str(line);
1646 result.push('\n');
1647 } else if target_style == CodeBlockStyle::Indented {
1648 } else {
1650 result.push_str(line);
1652 result.push('\n');
1653 }
1654 current_block_must_stay_fenced = false;
1655 current_fence_indent = 0;
1656 current_fence_baseline = 0;
1657 current_block_indented_prefix.clear();
1658 } else if current_block_must_stay_fenced {
1659 result.push_str(line);
1661 result.push('\n');
1662 } else if target_style == CodeBlockStyle::Indented {
1663 if !line.is_empty() {
1670 let body = Self::strip_indentation_columns(line, current_fence_indent);
1675 result.push_str(¤t_block_indented_prefix);
1676 result.push_str(&body);
1677 }
1678 result.push('\n');
1679 } else {
1680 result.push_str(line);
1682 result.push('\n');
1683 }
1684 } else if block_lines[i] {
1685 if ctx.inline_config().is_rule_disabled(self.name(), line_num) {
1689 result.push_str(line);
1690 result.push('\n');
1691 continue;
1692 }
1693
1694 let prev_line_is_indented = i > 0 && block_lines[i - 1];
1696 if !prev_line_is_indented {
1697 current_block = Some(i);
1698 }
1699 line_blocks[i] = current_block;
1700
1701 if target_style == CodeBlockStyle::Fenced {
1702 let baseline = ictx.list_item_baseline.get(i).copied().flatten().unwrap_or(0);
1708 let body = if line.trim().is_empty() {
1716 String::new()
1717 } else {
1718 Self::strip_indentation_columns(line, 4)
1719 };
1720
1721 if misplaced_fence_lines[i] {
1724 result.push_str(line.trim_start());
1726 result.push('\n');
1727 } else if unsafe_fence_lines[i] {
1728 result.push_str(line);
1731 result.push('\n');
1732 } else if !prev_line_is_indented && !in_indented_block {
1733 current_block_fence_indent = " ".repeat(baseline);
1735 result.push_str(¤t_block_fence_indent);
1736 result.push_str(Self::FENCE);
1737 result.push('\n');
1738 result.push_str(&body);
1739 result.push('\n');
1740 in_indented_block = true;
1741 } else {
1742 result.push_str(&body);
1744 result.push('\n');
1745 }
1746
1747 let next_line_is_indented = i < lines.len() - 1 && block_lines[i + 1];
1749 if !next_line_is_indented
1751 && in_indented_block
1752 && !misplaced_fence_lines[i]
1753 && !unsafe_fence_lines[i]
1754 {
1755 result.push_str(¤t_block_fence_indent);
1756 result.push_str(Self::FENCE);
1757 result.push('\n');
1758 in_indented_block = false;
1759 current_block_fence_indent.clear();
1760 }
1761 } else {
1762 result.push_str(line);
1764 result.push('\n');
1765 }
1766 } else {
1767 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1769 result.push_str(¤t_block_fence_indent);
1770 result.push_str(Self::FENCE);
1771 result.push('\n');
1772 in_indented_block = false;
1773 current_block_fence_indent.clear();
1774 output_starts[i] = result.len();
1776 }
1777 current_block = None;
1778
1779 result.push_str(line);
1780 result.push('\n');
1781 }
1782 }
1783
1784 if in_indented_block && target_style == CodeBlockStyle::Fenced {
1786 result.push_str(¤t_block_fence_indent);
1787 result.push_str(Self::FENCE);
1788 result.push('\n');
1789 }
1790 output_starts.push(result.len());
1791
1792 if !content.ends_with('\n') && result.ends_with('\n') {
1794 result.pop();
1795 }
1796
1797 if retained_structurally_unsafe_fence && self.config.style == CodeBlockStyle::Consistent {
1798 return Self::new(CodeBlockStyle::Fenced).convert_closed_blocks(ctx);
1799 }
1800
1801 if converted_fenced_to_indented {
1802 let reparsed_block_count = crate::utils::CodeBlockUtils::detect_code_blocks(&result).len();
1803 if reparsed_block_count != ctx.code_block_details.len() {
1804 if self.config.style == CodeBlockStyle::Consistent {
1809 return Self::new(CodeBlockStyle::Fenced).convert_closed_blocks(ctx);
1810 }
1811
1812 return Ok(Conversion::unchanged(content));
1813 }
1814 }
1815
1816 if result == content || (content.contains('\r') && result == content.replace("\r\n", "\n")) {
1817 return Ok(Conversion::unchanged(content));
1818 }
1819
1820 let edits = Self::block_edits(ctx, &result, &output_starts, &line_blocks);
1821 Ok(Conversion {
1822 content: crate::utils::ensure_consistent_line_endings(content, &result),
1823 edits,
1824 })
1825 }
1826
1827 fn block_edits(
1831 ctx: &crate::lint_context::LintContext,
1832 result: &str,
1833 output_starts: &[usize],
1834 line_blocks: &[Option<usize>],
1835 ) -> Vec<BlockEdit> {
1836 let content = ctx.content;
1837 let line_count = line_blocks.len();
1838 let line_start = |i: usize| {
1839 if i < line_count {
1840 ctx.line_offsets[i]
1841 } else {
1842 content.len()
1843 }
1844 };
1845
1846 let mut edits = Vec::new();
1847 let mut i = 0;
1848 while i < line_count {
1849 let Some(block) = line_blocks[i] else {
1850 i += 1;
1851 continue;
1852 };
1853 let first = i;
1854 while i < line_count && line_blocks[i] == Some(block) {
1855 i += 1;
1856 }
1857 let last = i - 1;
1858
1859 let original = &content[line_start(first)..line_start(i)];
1860 let replacement = &result[output_starts[first]..output_starts[i].min(result.len())];
1863 if original.replace("\r\n", "\n") == replacement {
1864 continue;
1865 }
1866 edits.push(BlockEdit {
1867 lines: first..=last,
1868 fix: Fix::new(
1869 line_start(first)..line_start(i),
1870 crate::utils::ensure_consistent_line_endings(content, replacement),
1871 ),
1872 });
1873 }
1874 edits
1875 }
1876}
1877
1878struct Conversion {
1880 content: String,
1881 edits: Vec<BlockEdit>,
1883}
1884
1885impl Conversion {
1886 fn unchanged(content: &str) -> Self {
1887 Self {
1888 content: content.to_string(),
1889 edits: Vec::new(),
1890 }
1891 }
1892}
1893
1894struct BlockEdit {
1896 lines: std::ops::RangeInclusive<usize>,
1897 fix: Fix,
1898}
1899
1900#[cfg(test)]
1901mod tests {
1902 use super::*;
1903 use crate::lint_context::LintContext;
1904
1905 fn detect_style_from_content(rule: &MD046CodeBlockStyle, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
1917 let flavor = if is_mkdocs {
1918 crate::config::MarkdownFlavor::MkDocs
1919 } else {
1920 crate::config::MarkdownFlavor::Standard
1921 };
1922 let ctx = LintContext::new(content, flavor, None);
1923 let lines: Vec<&str> = content.lines().collect();
1924 let in_list_context = rule.precompute_block_continuation_context(&lines);
1925 let in_tab_context = if is_mkdocs {
1926 rule.precompute_mkdocs_tab_context(&lines)
1927 } else {
1928 vec![false; lines.len()]
1929 };
1930 let in_admonition_context = if is_mkdocs {
1931 rule.precompute_mkdocs_admonition_context(&lines)
1932 } else {
1933 vec![false; lines.len()]
1934 };
1935 let in_comment_or_html = vec![false; lines.len()];
1936 let list_item_baseline: Vec<Option<usize>> = vec![None; lines.len()];
1942 let ictx = IndentContext {
1943 in_list_context: &in_list_context,
1944 in_tab_context: &in_tab_context,
1945 in_admonition_context: &in_admonition_context,
1946 in_comment_or_html: &in_comment_or_html,
1947 list_item_baseline: &list_item_baseline,
1948 };
1949 rule.detect_style(&ctx, &lines, is_mkdocs, &ictx)
1950 }
1951
1952 #[test]
1953 fn test_unclosed_fence_diagnostic_matches_document_fix() {
1954 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1955 for (content, expected) in [
1956 ("```", "```\n```"),
1957 ("```\ncode\n", "```\ncode\n```\n"),
1958 ("````\ncode\n```\n", "````\ncode\n```\n````\n"),
1959 ("> ```rust\n> code\n", "> ```rust\n> code\n> ```\n"),
1960 ("> > ~~~~\n> > code", "> > ~~~~\n> > code\n> > ~~~~"),
1961 ("- ```\n code\n", "- ```\n code\n ```\n"),
1962 (" ```\n code\n", " ```\n code\n ```\n"),
1963 ] {
1964 for newline in ["\n", "\r\n"] {
1965 let content = content.replace('\n', newline);
1966 let expected = if content.contains('\n') {
1967 expected.replace('\n', newline)
1968 } else {
1969 expected.to_string()
1970 };
1971 let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
1972 let warnings = rule.check(&ctx).unwrap();
1973 assert_eq!(warnings.len(), 1, "{content:?}: {warnings:?}");
1974 let edited = crate::utils::fix_utils::apply_warning_fixes(&content, &warnings).unwrap();
1975 assert_eq!(edited, expected, "{content:?}");
1976 assert_eq!(rule.fix(&ctx).unwrap(), expected, "{content:?}");
1977 let fixed_ctx = LintContext::new(&expected, crate::config::MarkdownFlavor::Standard, None);
1978 assert!(rule.check(&fixed_ctx).unwrap().is_empty(), "{expected:?}");
1979 assert_eq!(rule.fix(&fixed_ctx).unwrap(), expected);
1980 }
1981 }
1982 }
1983
1984 #[test]
1985 fn test_closed_quote_fence_at_eof_is_unchanged() {
1986 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1987 for content in [
1988 "> - item\n> ```\n> code\n> ```",
1989 "> > ~~~~\n> > code\n> > ~~~~",
1990 "```\r\ncode\r\n```\r\n",
1991 "Text\r\n\n- item\r\n",
1992 ] {
1993 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1994 let warnings = rule.check(&ctx).unwrap();
1995 assert!(warnings.is_empty(), "{content:?}: {warnings:?}");
1996 assert_eq!(rule.fix(&ctx).unwrap(), content);
1997 }
1998 }
1999
2000 #[test]
2001 fn test_unclosed_quote_repair_keeps_unsupported_style_warning() {
2002 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2003 let content = "> ```\n> code\n";
2004 let expected = "> ```\n> code\n> ```\n";
2005 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2006 let warnings = rule.check(&ctx).unwrap();
2007 assert_eq!(warnings.len(), 1);
2008 assert_eq!(
2009 crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap(),
2010 expected
2011 );
2012 assert_eq!(rule.fix(&ctx).unwrap(), expected);
2013 let fixed_ctx = LintContext::new(expected, crate::config::MarkdownFlavor::Standard, None);
2014 let remaining = rule.check(&fixed_ctx).unwrap();
2015 assert_eq!(remaining.len(), 1);
2016 assert_eq!(remaining[0].message, "Use indented code blocks");
2017 assert!(
2018 remaining[0].fix.is_none(),
2019 "Container conversion is intentionally unsupported"
2020 );
2021 assert_eq!(rule.fix(&fixed_ctx).unwrap(), expected);
2022 }
2023
2024 #[test]
2025 fn test_unclosed_fence_diagnostic_includes_indented_conversion() {
2026 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2027 let content = "```\ncode\n";
2028 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2029 let warnings = rule.check(&ctx).unwrap();
2030 assert_eq!(warnings.len(), 1);
2031 let edited = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
2032 assert_eq!(edited, " code\n");
2033 assert_eq!(rule.fix(&ctx).unwrap(), edited);
2034 let fixed_ctx = LintContext::new(&edited, crate::config::MarkdownFlavor::Standard, None);
2035 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
2036 assert_eq!(rule.fix(&fixed_ctx).unwrap(), edited);
2037 }
2038
2039 #[test]
2040 fn test_fenced_code_block_detection() {
2041 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2042 assert!(rule.is_fenced_code_block_start("```"));
2043 assert!(rule.is_fenced_code_block_start("```rust"));
2044 assert!(rule.is_fenced_code_block_start("~~~"));
2045 assert!(rule.is_fenced_code_block_start("~~~python"));
2046 assert!(rule.is_fenced_code_block_start(" ```"));
2047 assert!(!rule.is_fenced_code_block_start("``"));
2048 assert!(!rule.is_fenced_code_block_start("~~"));
2049 assert!(!rule.is_fenced_code_block_start("Regular text"));
2050 }
2051
2052 #[test]
2053 fn test_fix_capability_is_conditional() {
2054 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2055 assert_eq!(rule.fix_capability(), FixCapability::ConditionallyFixable);
2056 }
2057
2058 #[test]
2059 fn test_consistent_style_with_fenced_blocks() {
2060 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2061 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
2062 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2063 let result = rule.check(&ctx).unwrap();
2064
2065 assert_eq!(result.len(), 0);
2067 }
2068
2069 #[test]
2070 fn test_consistent_style_with_indented_blocks() {
2071 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2072 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
2073 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2074 let result = rule.check(&ctx).unwrap();
2075
2076 assert_eq!(result.len(), 0);
2078 }
2079
2080 #[test]
2081 fn test_consistent_style_mixed() {
2082 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2083 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
2084 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2085 let result = rule.check(&ctx).unwrap();
2086
2087 assert!(!result.is_empty());
2089 }
2090
2091 #[test]
2092 fn test_fenced_style_with_indented_blocks() {
2093 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2094 let content = "Text\n\n indented code\n more code\n\nMore text";
2095 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2096 let result = rule.check(&ctx).unwrap();
2097
2098 assert!(!result.is_empty());
2100 assert!(result[0].message.contains("Use fenced code blocks"));
2101 }
2102
2103 #[test]
2104 fn test_fenced_style_with_tab_indented_blocks() {
2105 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2106 let content = "Text\n\n\ttab indented code\n\tmore code\n\nMore text";
2107 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2108 let result = rule.check(&ctx).unwrap();
2109
2110 assert!(!result.is_empty());
2112 assert!(result[0].message.contains("Use fenced code blocks"));
2113 }
2114
2115 #[test]
2116 fn test_fenced_style_with_mixed_whitespace_indented_blocks() {
2117 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2118 let content = "Text\n\n \tmixed indent code\n \tmore code\n\nMore text";
2120 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2121 let result = rule.check(&ctx).unwrap();
2122
2123 assert!(
2125 !result.is_empty(),
2126 "Mixed whitespace (2 spaces + tab) should be detected as indented code"
2127 );
2128 assert!(result[0].message.contains("Use fenced code blocks"));
2129 }
2130
2131 #[test]
2132 fn test_fenced_style_with_one_space_tab_indent() {
2133 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2134 let content = "Text\n\n \ttab after one space\n \tmore code\n\nMore text";
2136 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2137 let result = rule.check(&ctx).unwrap();
2138
2139 assert!(!result.is_empty(), "1 space + tab should be detected as indented code");
2140 assert!(result[0].message.contains("Use fenced code blocks"));
2141 }
2142
2143 #[test]
2144 fn test_indented_style_with_fenced_blocks() {
2145 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2146 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
2147 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2148 let result = rule.check(&ctx).unwrap();
2149
2150 assert!(!result.is_empty());
2152 assert!(result[0].message.contains("Use indented code blocks"));
2153 }
2154
2155 #[test]
2156 fn test_unclosed_code_block() {
2157 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2158 let content = "```\ncode without closing fence";
2159 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2160 let result = rule.check(&ctx).unwrap();
2161
2162 assert_eq!(result.len(), 1);
2163 assert!(result[0].message.contains("never closed"));
2164 }
2165
2166 #[test]
2167 fn test_nested_code_blocks() {
2168 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2169 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
2170 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2171 let result = rule.check(&ctx).unwrap();
2172
2173 assert_eq!(result.len(), 0);
2175 }
2176
2177 #[test]
2178 fn test_fix_indented_to_fenced() {
2179 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2180 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
2181 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2182 let fixed = rule.fix(&ctx).unwrap();
2183
2184 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
2185 }
2186
2187 #[test]
2188 fn test_fix_fenced_to_indented() {
2189 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2190 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
2191 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2192 let fixed = rule.fix(&ctx).unwrap();
2193
2194 assert!(fixed.contains(" code line 1\n code line 2"));
2195 assert!(!fixed.contains("```"));
2196 }
2197
2198 #[test]
2199 fn test_fix_fenced_to_indented_blank_lines_have_no_trailing_spaces() {
2200 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2204 let content = "Text\n\n```\ncode line 1\n\ncode line 2\n```\n\nMore text";
2205 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2206 let fixed = rule.fix(&ctx).unwrap();
2207
2208 for line in fixed.lines() {
2209 assert!(
2210 line.is_empty() || !line.trim_end().is_empty() || line == line.trim_end(),
2211 "no line may have trailing whitespace, got {line:?}"
2212 );
2213 assert_ne!(line, " ", "blank line was indented to trailing spaces");
2214 }
2215 assert!(fixed.contains(" code line 1\n\n code line 2"));
2217 }
2218
2219 #[test]
2220 fn test_is_list_item_requires_delimiter_after_digits() {
2221 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2222 assert!(rule.is_list_item("1. First"));
2224 assert!(rule.is_list_item("42) Item"));
2225 assert!(rule.is_list_item(" 3. Indented item"));
2226 assert!(rule.is_list_item("- bullet"));
2228 assert!(rule.is_list_item("* bullet"));
2229 assert!(!rule.is_list_item("2 results. More info."));
2232 assert!(!rule.is_list_item("3 options (a, b) here"));
2233 assert!(!rule.is_list_item("100 items in stock. Buy now"));
2234 }
2235
2236 #[test]
2237 fn test_fix_fenced_to_indented_preserves_internal_indentation() {
2238 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2241 let content = r#"# Test
2242
2243```
2244<!doctype html>
2245<html>
2246 <head>
2247 <title>Test</title>
2248 </head>
2249</html>
2250```
2251"#;
2252 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2253 let fixed = rule.fix(&ctx).unwrap();
2254
2255 assert!(
2258 fixed.contains(" <head>"),
2259 "Expected 6 spaces before <head> (4 for code block + 2 original), got:\n{fixed}"
2260 );
2261 assert!(
2262 fixed.contains(" <title>"),
2263 "Expected 8 spaces before <title> (4 for code block + 4 original), got:\n{fixed}"
2264 );
2265 assert!(!fixed.contains("```"), "Fenced markers should be removed");
2266 }
2267
2268 #[test]
2269 fn test_fix_fenced_to_indented_preserves_python_indentation() {
2270 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2272 let content = r#"# Python Example
2273
2274```
2275def greet(name):
2276 if name:
2277 print(f"Hello, {name}!")
2278 else:
2279 print("Hello, World!")
2280```
2281"#;
2282 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2283 let fixed = rule.fix(&ctx).unwrap();
2284
2285 assert!(
2287 fixed.contains(" def greet(name):"),
2288 "Function def should have 4 spaces (code block indent)"
2289 );
2290 assert!(
2291 fixed.contains(" if name:"),
2292 "if statement should have 8 spaces (4 code + 4 Python)"
2293 );
2294 assert!(
2295 fixed.contains(" print"),
2296 "print should have 12 spaces (4 code + 8 Python)"
2297 );
2298 }
2299
2300 #[test]
2301 fn test_fix_fenced_to_indented_preserves_yaml_indentation() {
2302 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2304 let content = r#"# Config
2305
2306```
2307server:
2308 host: localhost
2309 port: 8080
2310 ssl:
2311 enabled: true
2312 cert: /path/to/cert
2313```
2314"#;
2315 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2316 let fixed = rule.fix(&ctx).unwrap();
2317
2318 assert!(fixed.contains(" server:"), "Root key should have 4 spaces");
2319 assert!(fixed.contains(" host:"), "First level should have 6 spaces");
2320 assert!(fixed.contains(" ssl:"), "ssl key should have 6 spaces");
2321 assert!(fixed.contains(" enabled:"), "Nested ssl should have 8 spaces");
2322 }
2323
2324 #[test]
2325 fn test_fix_fenced_to_indented_preserves_empty_lines() {
2326 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2330 let content = "```\nline1\n\nline2\n```\n";
2331 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2332 let fixed = rule.fix(&ctx).unwrap();
2333
2334 assert!(fixed.contains(" line1"), "line1 should be indented");
2336 assert!(fixed.contains(" line2"), "line2 should be indented");
2337 assert!(
2338 fixed.contains(" line1\n\n line2"),
2339 "blank line must stay empty, got {fixed:?}"
2340 );
2341 }
2342
2343 #[test]
2344 fn test_fix_fenced_to_indented_multiple_blocks() {
2345 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
2347 let content = r#"# Doc
2348
2349```
2350def foo():
2351 pass
2352```
2353
2354Text between.
2355
2356```
2357key:
2358 value: 1
2359```
2360"#;
2361 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2362 let fixed = rule.fix(&ctx).unwrap();
2363
2364 assert!(fixed.contains(" def foo():"), "Python def should be indented");
2365 assert!(fixed.contains(" pass"), "Python body should have 8 spaces");
2366 assert!(fixed.contains(" key:"), "YAML root should have 4 spaces");
2367 assert!(fixed.contains(" value:"), "YAML nested should have 6 spaces");
2368 assert!(!fixed.contains("```"), "No fence markers should remain");
2369 }
2370
2371 #[test]
2372 fn test_fix_unclosed_block() {
2373 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2374 let content = "```\ncode without closing";
2375 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2376 let fixed = rule.fix(&ctx).unwrap();
2377
2378 assert!(fixed.ends_with("```"));
2380 }
2381
2382 #[test]
2383 fn test_code_block_in_list() {
2384 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2385 let content = "- List item\n code in list\n more code\n- Next item";
2386 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2387 let result = rule.check(&ctx).unwrap();
2388
2389 assert_eq!(result.len(), 0);
2391 }
2392
2393 #[test]
2394 fn test_detect_style_fenced() {
2395 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2396 let content = "```\ncode\n```";
2397 let style = detect_style_from_content(&rule, content, false);
2398
2399 assert_eq!(style, Some(CodeBlockStyle::Fenced));
2400 }
2401
2402 #[test]
2403 fn test_detect_style_indented() {
2404 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2405 let content = "Text\n\n code\n\nMore";
2406 let style = detect_style_from_content(&rule, content, false);
2407
2408 assert_eq!(style, Some(CodeBlockStyle::Indented));
2409 }
2410
2411 #[test]
2412 fn test_detect_style_none() {
2413 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2414 let content = "No code blocks here";
2415 let style = detect_style_from_content(&rule, content, false);
2416
2417 assert_eq!(style, None);
2418 }
2419
2420 #[test]
2421 fn test_tilde_fence() {
2422 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2423 let content = "~~~\ncode\n~~~";
2424 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2425 let result = rule.check(&ctx).unwrap();
2426
2427 assert_eq!(result.len(), 0);
2429 }
2430
2431 #[test]
2432 fn test_language_specification() {
2433 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2434 let content = "```rust\nfn main() {}\n```";
2435 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2436 let result = rule.check(&ctx).unwrap();
2437
2438 assert_eq!(result.len(), 0);
2439 }
2440
2441 #[test]
2442 fn test_empty_content() {
2443 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2444 let content = "";
2445 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2446 let result = rule.check(&ctx).unwrap();
2447
2448 assert_eq!(result.len(), 0);
2449 }
2450
2451 #[test]
2452 fn test_default_config() {
2453 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2454 let (name, _config) = rule.default_config_section().unwrap();
2455 assert_eq!(name, "MD046");
2456 }
2457
2458 #[test]
2459 fn test_markdown_documentation_block() {
2460 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2461 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
2462 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2463 let result = rule.check(&ctx).unwrap();
2464
2465 assert_eq!(result.len(), 0);
2467 }
2468
2469 #[test]
2470 fn test_preserve_trailing_newline() {
2471 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2472 let content = "```\ncode\n```\n";
2473 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2474 let fixed = rule.fix(&ctx).unwrap();
2475
2476 assert_eq!(fixed, content);
2477 }
2478
2479 #[test]
2480 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
2481 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2482 let content = r#"# Document
2483
2484=== "Python"
2485
2486 This is tab content
2487 Not an indented code block
2488
2489 ```python
2490 def hello():
2491 print("Hello")
2492 ```
2493
2494=== "JavaScript"
2495
2496 More tab content here
2497 Also not an indented code block"#;
2498
2499 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2500 let result = rule.check(&ctx).unwrap();
2501
2502 assert_eq!(result.len(), 0);
2504 }
2505
2506 #[test]
2507 fn test_mkdocs_tabs_with_actual_indented_code() {
2508 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2509 let content = r#"# Document
2510
2511=== "Tab 1"
2512
2513 This is tab content
2514
2515Regular text
2516
2517 This is an actual indented code block
2518 Should be flagged"#;
2519
2520 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2521 let result = rule.check(&ctx).unwrap();
2522
2523 assert_eq!(result.len(), 1);
2525 assert!(result[0].message.contains("Use fenced code blocks"));
2526 }
2527
2528 #[test]
2529 fn test_mkdocs_tabs_detect_style() {
2530 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
2531 let content = r#"=== "Tab 1"
2532
2533 Content in tab
2534 More content
2535
2536=== "Tab 2"
2537
2538 Content in second tab"#;
2539
2540 let style = detect_style_from_content(&rule, content, true);
2542 assert_eq!(style, None); let style = detect_style_from_content(&rule, content, false);
2546 assert_eq!(style, Some(CodeBlockStyle::Indented));
2547 }
2548
2549 #[test]
2550 fn test_mkdocs_nested_tabs() {
2551 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2552 let content = r#"# Document
2553
2554=== "Outer Tab"
2555
2556 Some content
2557
2558 === "Nested Tab"
2559
2560 Nested tab content
2561 Should not be flagged"#;
2562
2563 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2564 let result = rule.check(&ctx).unwrap();
2565
2566 assert_eq!(result.len(), 0);
2568 }
2569
2570 #[test]
2571 fn test_mkdocs_admonitions_not_flagged_as_indented_code() {
2572 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2575 let content = r#"# Document
2576
2577!!! note
2578 This is normal admonition content, not a code block.
2579 It spans multiple lines.
2580
2581??? warning "Collapsible Warning"
2582 This is also admonition content.
2583
2584???+ tip "Expanded Tip"
2585 And this one too.
2586
2587Regular text outside admonitions."#;
2588
2589 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2590 let result = rule.check(&ctx).unwrap();
2591
2592 assert_eq!(
2594 result.len(),
2595 0,
2596 "Admonition content in MkDocs mode should not trigger MD046"
2597 );
2598 }
2599
2600 #[test]
2601 fn test_mkdocs_admonition_with_actual_indented_code() {
2602 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2604 let content = r#"# Document
2605
2606!!! note
2607 This is admonition content.
2608
2609Regular text ends the admonition.
2610
2611 This is actual indented code (should be flagged)"#;
2612
2613 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2614 let result = rule.check(&ctx).unwrap();
2615
2616 assert_eq!(result.len(), 1);
2618 assert!(result[0].message.contains("Use fenced code blocks"));
2619 }
2620
2621 #[test]
2622 fn test_admonition_in_standard_mode_flagged() {
2623 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2627 let content = r#"# Document
2628
2629!!! note
2630
2631 This looks like code in standard mode.
2632
2633Regular text."#;
2634
2635 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2637 let result = rule.check(&ctx).unwrap();
2638
2639 assert_eq!(
2641 result.len(),
2642 1,
2643 "Admonition content in Standard mode should be flagged as indented code"
2644 );
2645 }
2646
2647 #[test]
2648 fn test_mkdocs_admonition_with_fenced_code_inside() {
2649 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2651 let content = r#"# Document
2652
2653!!! note "Code Example"
2654 Here's some code:
2655
2656 ```python
2657 def hello():
2658 print("world")
2659 ```
2660
2661 More text after code.
2662
2663Regular text."#;
2664
2665 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2666 let result = rule.check(&ctx).unwrap();
2667
2668 assert_eq!(result.len(), 0, "Fenced code blocks inside admonitions should be valid");
2670 }
2671
2672 #[test]
2673 fn test_mkdocs_nested_admonitions() {
2674 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2676 let content = r#"# Document
2677
2678!!! note "Outer"
2679 Outer content.
2680
2681 !!! warning "Inner"
2682 Inner content.
2683 More inner content.
2684
2685 Back to outer.
2686
2687Regular text."#;
2688
2689 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2690 let result = rule.check(&ctx).unwrap();
2691
2692 assert_eq!(result.len(), 0, "Nested admonitions should not be flagged");
2694 }
2695
2696 #[test]
2697 fn test_mkdocs_admonition_fix_does_not_wrap() {
2698 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2700 let content = r#"!!! note
2701 Content that should stay as admonition content.
2702 Not be wrapped in code fences.
2703"#;
2704
2705 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2706 let fixed = rule.fix(&ctx).unwrap();
2707
2708 assert!(
2710 !fixed.contains("```\n Content"),
2711 "Admonition content should not be wrapped in fences"
2712 );
2713 assert_eq!(fixed, content, "Content should remain unchanged");
2714 }
2715
2716 #[test]
2717 fn test_mkdocs_empty_admonition() {
2718 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2720 let content = r#"!!! note
2721
2722Regular paragraph after empty admonition.
2723
2724 This IS an indented code block (after blank + non-indented line)."#;
2725
2726 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2727 let result = rule.check(&ctx).unwrap();
2728
2729 assert_eq!(result.len(), 1, "Indented code after admonition ends should be flagged");
2731 }
2732
2733 #[test]
2734 fn test_mkdocs_indented_admonition() {
2735 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2737 let content = r#"- List item
2738
2739 !!! note
2740 Indented admonition content.
2741 More content.
2742
2743- Next item"#;
2744
2745 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
2746 let result = rule.check(&ctx).unwrap();
2747
2748 assert_eq!(
2750 result.len(),
2751 0,
2752 "Indented admonitions (e.g., in lists) should not be flagged"
2753 );
2754 }
2755
2756 #[test]
2757 fn test_footnote_indented_paragraphs_not_flagged() {
2758 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2759 let content = r#"# Test Document with Footnotes
2760
2761This is some text with a footnote[^1].
2762
2763Here's some code:
2764
2765```bash
2766echo "fenced code block"
2767```
2768
2769More text with another footnote[^2].
2770
2771[^1]: Really interesting footnote text.
2772
2773 Even more interesting second paragraph.
2774
2775[^2]: Another footnote.
2776
2777 With a second paragraph too.
2778
2779 And even a third paragraph!"#;
2780
2781 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2782 let result = rule.check(&ctx).unwrap();
2783
2784 assert_eq!(result.len(), 0);
2786 }
2787
2788 #[test]
2789 fn test_footnote_definition_detection() {
2790 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2791
2792 assert!(rule.is_footnote_definition("[^1]: Footnote text"));
2795 assert!(rule.is_footnote_definition("[^foo]: Footnote text"));
2796 assert!(rule.is_footnote_definition("[^long-name]: Footnote text"));
2797 assert!(rule.is_footnote_definition("[^test_123]: Mixed chars"));
2798 assert!(rule.is_footnote_definition(" [^1]: Indented footnote"));
2799 assert!(rule.is_footnote_definition("[^a]: Minimal valid footnote"));
2800 assert!(rule.is_footnote_definition("[^123]: Numeric label"));
2801 assert!(rule.is_footnote_definition("[^_]: Single underscore"));
2802 assert!(rule.is_footnote_definition("[^-]: Single hyphen"));
2803
2804 assert!(!rule.is_footnote_definition("[^]: No label"));
2806 assert!(!rule.is_footnote_definition("[^ ]: Whitespace only"));
2807 assert!(!rule.is_footnote_definition("[^ ]: Multiple spaces"));
2808 assert!(!rule.is_footnote_definition("[^\t]: Tab only"));
2809
2810 assert!(!rule.is_footnote_definition("[^]]: Extra bracket"));
2812 assert!(!rule.is_footnote_definition("Regular text [^1]:"));
2813 assert!(!rule.is_footnote_definition("[1]: Not a footnote"));
2814 assert!(!rule.is_footnote_definition("[^")); assert!(!rule.is_footnote_definition("[^1:")); assert!(!rule.is_footnote_definition("^1]: Missing opening bracket"));
2817
2818 assert!(!rule.is_footnote_definition("[^test.name]: Period"));
2820 assert!(!rule.is_footnote_definition("[^test name]: Space in label"));
2821 assert!(!rule.is_footnote_definition("[^test@name]: Special char"));
2822 assert!(!rule.is_footnote_definition("[^test/name]: Slash"));
2823 assert!(!rule.is_footnote_definition("[^test\\name]: Backslash"));
2824
2825 assert!(!rule.is_footnote_definition("[^test\r]: Carriage return"));
2828 }
2829
2830 #[test]
2831 fn test_footnote_with_blank_lines() {
2832 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2836 let content = r#"# Document
2837
2838Text with footnote[^1].
2839
2840[^1]: First paragraph.
2841
2842 Second paragraph after blank line.
2843
2844 Third paragraph after another blank line.
2845
2846Regular text at column 0 ends the footnote."#;
2847
2848 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2849 let result = rule.check(&ctx).unwrap();
2850
2851 assert_eq!(
2853 result.len(),
2854 0,
2855 "Indented content within footnotes should not trigger MD046"
2856 );
2857 }
2858
2859 #[test]
2860 fn test_footnote_multiple_consecutive_blank_lines() {
2861 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2864 let content = r#"Text[^1].
2865
2866[^1]: First paragraph.
2867
2868
2869
2870 Content after three blank lines (still part of footnote).
2871
2872Not indented, so footnote ends here."#;
2873
2874 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2875 let result = rule.check(&ctx).unwrap();
2876
2877 assert_eq!(
2879 result.len(),
2880 0,
2881 "Multiple blank lines shouldn't break footnote continuation"
2882 );
2883 }
2884
2885 #[test]
2886 fn test_footnote_terminated_by_non_indented_content() {
2887 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2890 let content = r#"[^1]: Footnote content.
2891
2892 More indented content in footnote.
2893
2894This paragraph is not indented, so footnote ends.
2895
2896 This should be flagged as indented code block."#;
2897
2898 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2899 let result = rule.check(&ctx).unwrap();
2900
2901 assert_eq!(
2903 result.len(),
2904 1,
2905 "Indented code after footnote termination should be flagged"
2906 );
2907 assert!(
2908 result[0].message.contains("Use fenced code blocks"),
2909 "Expected MD046 warning for indented code block"
2910 );
2911 assert!(result[0].line >= 7, "Warning should be on the indented code block line");
2912 }
2913
2914 #[test]
2915 fn test_footnote_terminated_by_structural_elements() {
2916 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2918 let content = r#"[^1]: Footnote content.
2919
2920 More content.
2921
2922## Heading terminates footnote
2923
2924 This indented content should be flagged.
2925
2926---
2927
2928 This should also be flagged (after horizontal rule)."#;
2929
2930 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2931 let result = rule.check(&ctx).unwrap();
2932
2933 assert_eq!(
2935 result.len(),
2936 2,
2937 "Both indented blocks after termination should be flagged"
2938 );
2939 }
2940
2941 #[test]
2942 fn test_footnote_with_code_block_inside() {
2943 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2946 let content = r#"Text[^1].
2947
2948[^1]: Footnote with code:
2949
2950 ```python
2951 def hello():
2952 print("world")
2953 ```
2954
2955 More footnote text after code."#;
2956
2957 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2958 let result = rule.check(&ctx).unwrap();
2959
2960 assert_eq!(result.len(), 0, "Fenced code blocks within footnotes should be allowed");
2962 }
2963
2964 #[test]
2965 fn test_footnote_with_8_space_indented_code() {
2966 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2969 let content = r#"Text[^1].
2970
2971[^1]: Footnote with nested code.
2972
2973 code block
2974 more code"#;
2975
2976 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2977 let result = rule.check(&ctx).unwrap();
2978
2979 assert_eq!(
2981 result.len(),
2982 0,
2983 "8-space indented code within footnotes represents nested code blocks"
2984 );
2985 }
2986
2987 #[test]
2988 fn test_multiple_footnotes() {
2989 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
2992 let content = r#"Text[^1] and more[^2].
2993
2994[^1]: First footnote.
2995
2996 Continuation of first.
2997
2998[^2]: Second footnote starts here, ending the first.
2999
3000 Continuation of second."#;
3001
3002 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3003 let result = rule.check(&ctx).unwrap();
3004
3005 assert_eq!(
3007 result.len(),
3008 0,
3009 "Multiple footnotes should each maintain their continuation context"
3010 );
3011 }
3012
3013 #[test]
3014 fn test_list_item_ends_footnote_context() {
3015 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3017 let content = r#"[^1]: Footnote.
3018
3019 Content in footnote.
3020
3021- List item starts here (ends footnote context).
3022
3023 This indented content is part of the list, not the footnote."#;
3024
3025 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3026 let result = rule.check(&ctx).unwrap();
3027
3028 assert_eq!(
3030 result.len(),
3031 0,
3032 "List items should end footnote context and start their own"
3033 );
3034 }
3035
3036 #[test]
3037 fn test_footnote_vs_actual_indented_code() {
3038 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3041 let content = r#"# Heading
3042
3043Text with footnote[^1].
3044
3045[^1]: Footnote content.
3046
3047 Part of footnote (should not be flagged).
3048
3049Regular paragraph ends footnote context.
3050
3051 This is actual indented code (MUST be flagged)
3052 Should be detected as code block"#;
3053
3054 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3055 let result = rule.check(&ctx).unwrap();
3056
3057 assert_eq!(
3059 result.len(),
3060 1,
3061 "Must still detect indented code blocks outside footnotes"
3062 );
3063 assert!(
3064 result[0].message.contains("Use fenced code blocks"),
3065 "Expected MD046 warning for indented code"
3066 );
3067 assert!(
3068 result[0].line >= 11,
3069 "Warning should be on the actual indented code line"
3070 );
3071 }
3072
3073 #[test]
3074 fn test_spec_compliant_label_characters() {
3075 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3078
3079 assert!(rule.is_footnote_definition("[^test]: text"));
3081 assert!(rule.is_footnote_definition("[^TEST]: text"));
3082 assert!(rule.is_footnote_definition("[^test-name]: text"));
3083 assert!(rule.is_footnote_definition("[^test_name]: text"));
3084 assert!(rule.is_footnote_definition("[^test123]: text"));
3085 assert!(rule.is_footnote_definition("[^123]: text"));
3086 assert!(rule.is_footnote_definition("[^a1b2c3]: text"));
3087
3088 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")); }
3096
3097 #[test]
3098 fn test_code_block_inside_html_comment() {
3099 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3102 let content = r#"# Document
3103
3104Some text.
3105
3106<!--
3107Example code block in comment:
3108
3109```typescript
3110console.log("Hello");
3111```
3112
3113More comment text.
3114-->
3115
3116More content."#;
3117
3118 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3119 let result = rule.check(&ctx).unwrap();
3120
3121 assert_eq!(
3122 result.len(),
3123 0,
3124 "Code blocks inside HTML comments should not be flagged as unclosed"
3125 );
3126 }
3127
3128 #[test]
3129 fn test_unclosed_fence_inside_html_comment() {
3130 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3132 let content = r#"# Document
3133
3134<!--
3135Example with intentionally unclosed fence:
3136
3137```
3138code without closing
3139-->
3140
3141More content."#;
3142
3143 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3144 let result = rule.check(&ctx).unwrap();
3145
3146 assert_eq!(
3147 result.len(),
3148 0,
3149 "Unclosed fences inside HTML comments should be ignored"
3150 );
3151 }
3152
3153 #[test]
3154 fn test_multiline_html_comment_with_indented_code() {
3155 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3157 let content = r#"# Document
3158
3159<!--
3160Example:
3161
3162 indented code
3163 more code
3164
3165End of comment.
3166-->
3167
3168Regular text."#;
3169
3170 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3171 let result = rule.check(&ctx).unwrap();
3172
3173 assert_eq!(
3174 result.len(),
3175 0,
3176 "Indented code inside HTML comments should not be flagged"
3177 );
3178 }
3179
3180 #[test]
3181 fn test_code_block_after_html_comment() {
3182 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3184 let content = r#"# Document
3185
3186<!-- comment -->
3187
3188Text before.
3189
3190 indented code should be flagged
3191
3192More text."#;
3193
3194 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3195 let result = rule.check(&ctx).unwrap();
3196
3197 assert_eq!(
3198 result.len(),
3199 1,
3200 "Code blocks after HTML comments should still be detected"
3201 );
3202 assert!(result[0].message.contains("Use fenced code blocks"));
3203 }
3204
3205 #[test]
3206 fn test_consistent_style_indented_html_comment() {
3207 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3213 let content = "# MD046 false-positive reproduction\n\
3214 \n\
3215 <!--\n \
3216 This is just an indented comment, not a code block.\n\
3217 \n \
3218 A second line is required to trigger the false-positive.\n\
3219 \n \
3220 Actually, three lines are required.\n\
3221 -->\n\
3222 \n\
3223 ```md\n\
3224 This should be fine, since it's the only code block and therefore consistent.\n\
3225 ```\n";
3226
3227 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3228 let result = rule.check(&ctx).unwrap();
3229
3230 assert_eq!(
3231 result,
3232 vec![],
3233 "A single fenced block and an indented HTML comment must produce no MD046 warnings",
3234 );
3235 }
3236
3237 #[test]
3238 fn test_consistent_style_indented_html_block() {
3239 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3246 let content = "# Heading\n\
3247 \n\
3248 <div class=\"note\">\n \
3249 line one of indented html content\n \
3250 line two of indented html content\n \
3251 line three of indented html content\n\
3252 </div>\n\
3253 \n\
3254 ```md\n\
3255 real fenced block\n\
3256 ```\n";
3257
3258 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3259 let result = rule.check(&ctx).unwrap();
3260
3261 assert_eq!(
3262 result,
3263 vec![],
3264 "Indented content inside a raw HTML block must not influence MD046 style detection",
3265 );
3266 }
3267
3268 #[test]
3269 fn test_consistent_style_fake_fence_inside_html_comment() {
3270 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3276 let content = "# Title\n\
3277 \n\
3278 <!--\n\
3279 ```\n\
3280 fake fence inside comment\n\
3281 ```\n\
3282 -->\n\
3283 \n \
3284 real indented code block line 1\n \
3285 real indented code block line 2\n";
3286
3287 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3288 let result = rule.check(&ctx).unwrap();
3289
3290 assert_eq!(
3291 result,
3292 vec![],
3293 "Fence markers inside an HTML comment must not influence MD046 style detection",
3294 );
3295 }
3296
3297 #[test]
3298 fn test_consistent_style_indented_footnote_definition() {
3299 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3303 let content = "# Heading\n\
3304 \n\
3305 Reference to a footnote[^note].\n\
3306 \n\
3307 [^note]: First line of the footnote.\n \
3308 Second indented continuation line.\n \
3309 Third indented continuation line.\n \
3310 Fourth indented continuation line.\n\
3311 \n\
3312 ```md\n\
3313 real fenced block\n\
3314 ```\n";
3315
3316 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3317 let result = rule.check(&ctx).unwrap();
3318
3319 assert_eq!(
3320 result,
3321 vec![],
3322 "Footnote-definition continuation content must not influence MD046 style detection",
3323 );
3324 }
3325
3326 #[test]
3327 fn test_consistent_style_indented_blockquote() {
3328 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3333 let content = "# Heading\n\
3334 \n\
3335 > line one of quoted indented content\n\
3336 >\n\
3337 > line two of quoted indented content\n\
3338 >\n\
3339 > line three of quoted indented content\n\
3340 \n\
3341 ```md\n\
3342 real fenced block\n\
3343 ```\n";
3344
3345 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3346 let result = rule.check(&ctx).unwrap();
3347
3348 assert_eq!(
3349 result,
3350 vec![],
3351 "Indented content inside a blockquote must not influence MD046 style detection",
3352 );
3353 }
3354
3355 #[test]
3356 fn test_consistent_style_genuine_indented_block_detected_as_indented() {
3357 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3362 let content = "# Heading\n\
3363 \n\
3364 Some prose.\n\
3365 \n \
3366 real indented code line 1\n \
3367 real indented code line 2\n";
3368
3369 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3370 let result = rule.check(&ctx).unwrap();
3371
3372 assert_eq!(
3375 result,
3376 vec![],
3377 "A genuine top-level indented block must be detected as Indented style under Consistent",
3378 );
3379 }
3380
3381 #[test]
3382 fn test_consistent_style_skipped_lines_dont_override_real_block() {
3383 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3388 let content = "# Heading\n\
3389 \n\
3390 <!--\n \
3391 skipped indented comment line 1\n \
3392 skipped indented comment line 2\n\
3393 -->\n\
3394 \n\
3395 <!--\n \
3396 second skipped region\n \
3397 also skipped\n\
3398 -->\n\
3399 \n \
3400 real indented code line\n";
3401
3402 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3403 let result = rule.check(&ctx).unwrap();
3404
3405 assert_eq!(
3406 result,
3407 vec![],
3408 "Skipped container lines must not outweigh the single real indented block",
3409 );
3410 }
3411
3412 #[test]
3413 fn test_consistent_style_fenced_wins_over_skipped_indented() {
3414 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3418 let content = "# Heading\n\
3419 \n\
3420 <!--\n \
3421 skipped indented region one\n \
3422 more of region one\n\
3423 -->\n\
3424 \n\
3425 <!--\n \
3426 skipped indented region two\n \
3427 more of region two\n\
3428 -->\n\
3429 \n\
3430 ```md\n\
3431 real fenced block\n\
3432 ```\n";
3433
3434 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3435 let result = rule.check(&ctx).unwrap();
3436
3437 assert_eq!(
3438 result,
3439 vec![],
3440 "Fenced block must win when all indented lines are inside skipped containers",
3441 );
3442 }
3443
3444 #[test]
3445 fn test_four_space_indented_fence_is_not_valid_fence() {
3446 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3449
3450 assert!(rule.is_fenced_code_block_start("```"));
3452 assert!(rule.is_fenced_code_block_start(" ```"));
3453 assert!(rule.is_fenced_code_block_start(" ```"));
3454 assert!(rule.is_fenced_code_block_start(" ```"));
3455
3456 assert!(!rule.is_fenced_code_block_start(" ```"));
3458 assert!(!rule.is_fenced_code_block_start(" ```"));
3459 assert!(!rule.is_fenced_code_block_start(" ```"));
3460
3461 assert!(!rule.is_fenced_code_block_start("\t```"));
3463 }
3464
3465 #[test]
3466 fn test_issue_237_indented_fenced_block_detected_as_indented() {
3467 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3473
3474 let content = r#"## Test
3476
3477 ```js
3478 var foo = "hello";
3479 ```
3480"#;
3481
3482 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3483 let result = rule.check(&ctx).unwrap();
3484
3485 assert_eq!(
3487 result.len(),
3488 1,
3489 "4-space indented fence should be detected as indented code block"
3490 );
3491 assert!(
3492 result[0].message.contains("Use fenced code blocks"),
3493 "Expected 'Use fenced code blocks' message"
3494 );
3495 }
3496
3497 #[test]
3498 fn test_issue_276_indented_code_in_list() {
3499 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3502
3503 let content = r#"1. First item
35042. Second item with code:
3505
3506 # This is a code block in a list
3507 print("Hello, world!")
3508
35094. Third item"#;
3510
3511 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3512 let result = rule.check(&ctx).unwrap();
3513
3514 assert!(
3516 !result.is_empty(),
3517 "Indented code block inside list should be flagged when style=fenced"
3518 );
3519 assert!(
3520 result[0].message.contains("Use fenced code blocks"),
3521 "Expected 'Use fenced code blocks' message"
3522 );
3523 }
3524
3525 #[test]
3526 fn test_three_space_indented_fence_is_valid() {
3527 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3529
3530 let content = r#"## Test
3531
3532 ```js
3533 var foo = "hello";
3534 ```
3535"#;
3536
3537 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3538 let result = rule.check(&ctx).unwrap();
3539
3540 assert_eq!(
3542 result.len(),
3543 0,
3544 "3-space indented fence should be recognized as valid fenced code block"
3545 );
3546 }
3547
3548 #[test]
3549 fn test_indented_style_with_deeply_indented_fenced() {
3550 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
3553
3554 let content = r#"Text
3555
3556 ```js
3557 var foo = "hello";
3558 ```
3559
3560More text
3561"#;
3562
3563 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3564 let result = rule.check(&ctx).unwrap();
3565
3566 assert_eq!(
3569 result.len(),
3570 0,
3571 "4-space indented content should be valid when style=indented"
3572 );
3573 }
3574
3575 #[test]
3576 fn test_fix_misplaced_fenced_block() {
3577 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3580
3581 let content = r#"## Test
3582
3583 ```js
3584 var foo = "hello";
3585 ```
3586"#;
3587
3588 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3589 let fixed = rule.fix(&ctx).unwrap();
3590
3591 let expected = r#"## Test
3593
3594```js
3595var foo = "hello";
3596```
3597"#;
3598
3599 assert_eq!(fixed, expected, "Fix should remove indentation, not add more fences");
3600 }
3601
3602 #[test]
3603 fn test_fix_regular_indented_block() {
3604 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3607
3608 let content = r#"Text
3609
3610 var foo = "hello";
3611 console.log(foo);
3612
3613More text
3614"#;
3615
3616 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3617 let fixed = rule.fix(&ctx).unwrap();
3618
3619 assert!(fixed.contains("```\nvar foo"), "Should add opening fence");
3621 assert!(fixed.contains("console.log(foo);\n```"), "Should add closing fence");
3622 }
3623
3624 #[test]
3625 fn test_fix_indented_block_with_fence_like_content() {
3626 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3630
3631 let content = r#"Text
3632
3633 some code
3634 ```not a fence opener
3635 more code
3636"#;
3637
3638 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3639 let fixed = rule.fix(&ctx).unwrap();
3640
3641 assert!(fixed.contains(" some code"), "Unsafe block should be left unchanged");
3643 assert!(!fixed.contains("```\nsome code"), "Should NOT wrap unsafe block");
3644 }
3645
3646 #[test]
3647 fn test_fix_mixed_indented_and_misplaced_blocks() {
3648 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3650
3651 let content = r#"Text
3652
3653 regular indented code
3654
3655More text
3656
3657 ```python
3658 print("hello")
3659 ```
3660"#;
3661
3662 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3663 let fixed = rule.fix(&ctx).unwrap();
3664
3665 assert!(
3667 fixed.contains("```\nregular indented code\n```"),
3668 "First block should be wrapped in fences"
3669 );
3670
3671 assert!(
3673 fixed.contains("\n```python\nprint(\"hello\")\n```"),
3674 "Second block should be dedented, not double-wrapped"
3675 );
3676 assert!(
3678 !fixed.contains("```\n```python"),
3679 "Should not have nested fence openers"
3680 );
3681 }
3682
3683 #[test]
3684 fn test_md046_front_matter() {
3685 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3686 let content = "---\nmetadata:\n\n description: Indented\n---\n";
3687 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3688 let result = rule.check(&ctx).unwrap();
3689 assert_eq!(result.len(), 0);
3690 }
3691
3692 #[test]
3693 fn test_md046_fix_front_matter() {
3694 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3695 let content = "---\nmetadata:\n\n description: Indented\n---\n";
3696 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3697 let fixed = rule.fix(&ctx).unwrap();
3698 assert_eq!(fixed, content);
3699 }
3700
3701 #[test]
3702 fn test_whitespace_only_line_is_not_an_indented_code_block() {
3703 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3708 let content = "# T\n\nPara\n\n \nMore\n\n real code\n\nEnd\n";
3709 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3710 let fixed = rule.fix(&ctx).unwrap();
3711 assert_eq!(fixed, "# T\n\nPara\n\n \nMore\n\n```\nreal code\n```\n\nEnd\n");
3712 }
3713
3714 #[test]
3715 fn test_interior_blank_line_keeps_indented_block_together() {
3716 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3720 let content = "# T\n\nPara\n\n a\n\n b\n\nAfter\n";
3721 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3722 let fixed = rule.fix(&ctx).unwrap();
3723 assert_eq!(fixed, "# T\n\nPara\n\n```\na\n\nb\n```\n\nAfter\n");
3724 }
3725
3726 #[test]
3727 fn test_consistent_style_counts_a_block_with_interior_blank_once() {
3728 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3732 let content = "# T\n\n```\nfenced\n```\n\nPara\n\n a\n\n b\n\nEnd\n";
3733 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3734 let result = rule.check(&ctx).unwrap();
3735 let reported: Vec<(usize, &str)> = result.iter().map(|w| (w.line, w.message.as_str())).collect();
3736 assert_eq!(reported, vec![(9, "Use fenced code blocks")]);
3737 }
3738
3739 #[test]
3740 fn test_indented_lazy_continuation_lines_are_not_code() {
3741 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3747 let content = "# T\n\nPara\n lazy one\n lazy two\n lazy three\n\n real code\n\nEnd\n";
3748 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3749 let fixed = rule.fix(&ctx).unwrap();
3750 assert_eq!(
3751 fixed,
3752 "# T\n\nPara\n lazy one\n lazy two\n lazy three\n\n```\nreal code\n```\n\nEnd\n"
3753 );
3754 }
3755
3756 #[test]
3757 fn test_misplaced_fence_with_interior_blank_dedents_as_one_block() {
3758 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3762 let content = "# T\n\nPara\n\n ```python\n x = 1\n\n y = 2\n ```\n\nAfter\n";
3763 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3764 let fixed = rule.fix(&ctx).unwrap();
3765 assert_eq!(fixed, "# T\n\nPara\n\n```python\nx = 1\n\ny = 2\n```\n\nAfter\n");
3766 }
3767 #[test]
3768 fn test_mdg_overrides_indented_style_to_fenced() {
3769 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
3774 let content = "# Feature: Payloads\n\n## Scenario: JSON payload\n\n* Given this payload\n\n ```json\n {\"ok\": true}\n ```\n";
3775
3776 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3777 assert!(rule.check(&mdg_ctx).unwrap().is_empty());
3778 assert_eq!(rule.fix(&mdg_ctx).unwrap(), content);
3779
3780 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3783 let standard_warnings = rule.check(&standard_ctx).unwrap();
3784 assert_eq!(standard_warnings.len(), 1);
3785 assert_eq!(standard_warnings[0].message, "Use indented code blocks");
3786 assert_eq!(rule.fix(&standard_ctx).unwrap(), content);
3787 }
3788
3789 #[test]
3790 fn test_mdg_indented_style_still_fences_indented_blocks() {
3791 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
3795 let content =
3796 "# Feature: Payloads\n\n## Scenario: Plain payload\n\nSome description.\n\n ordinary indented code\n";
3797
3798 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3799 let warnings = rule.check(&mdg_ctx).unwrap();
3800 assert_eq!(warnings.len(), 1);
3801 assert_eq!(warnings[0].message, "Use fenced code blocks");
3802
3803 let fixed = rule.fix(&mdg_ctx).unwrap();
3804 assert_eq!(
3805 fixed,
3806 "# Feature: Payloads\n\n## Scenario: Plain payload\n\nSome description.\n\n```\n ordinary indented code\n```\n"
3807 );
3808
3809 let fixed_ctx = LintContext::new(&fixed, crate::config::MarkdownFlavor::MDG, None);
3810 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
3811 assert_eq!(rule.fix(&fixed_ctx).unwrap(), fixed, "MDG fix should be idempotent");
3812
3813 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3816 assert!(rule.check(&standard_ctx).unwrap().is_empty());
3817 assert_eq!(rule.fix(&standard_ctx).unwrap(), content);
3818 }
3819
3820 #[test]
3821 fn test_mdg_steers_indented_code_to_fenced() {
3822 let content = "# Feature: Payloads\n\n## Scenario: Plain payload\n\n* Given this payload\n\n ordinary indented code\n";
3826
3827 for rule in [
3828 MD046CodeBlockStyle::new(CodeBlockStyle::Fenced),
3829 MD046CodeBlockStyle::new(CodeBlockStyle::Consistent),
3830 MD046CodeBlockStyle::new(CodeBlockStyle::Indented),
3831 ] {
3832 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3833 let warnings = rule.check(&ctx).unwrap();
3834 assert_eq!(warnings.len(), 1);
3835 assert_eq!(warnings[0].message, "Use fenced code blocks");
3836
3837 let fixed = rule.fix(&ctx).unwrap();
3838 assert!(fixed.contains("```"), "MDG must fence the block: {fixed:?}");
3839
3840 let fixed_ctx = LintContext::new(&fixed, crate::config::MarkdownFlavor::MDG, None);
3841 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
3842 assert_eq!(rule.fix(&fixed_ctx).unwrap(), fixed, "MDG fix should be idempotent");
3843 }
3844 }
3845
3846 #[test]
3847 fn test_mdg_consistent_style_ignores_indented_prevalence() {
3848 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
3851 let indented_majority = "# Feature: Payloads\n\n## Scenario: Mixed payloads\n\n* Given this payload\n\n```\n{\"ok\": true}\n```\n\nFirst ordinary example:\n\n one\n\nSecond ordinary example:\n\n two\n";
3852
3853 let standard_ctx = LintContext::new(indented_majority, crate::config::MarkdownFlavor::Standard, None);
3854 let standard_warnings = rule.check(&standard_ctx).unwrap();
3855 assert_eq!(standard_warnings.len(), 1);
3856 assert_eq!(standard_warnings[0].message, "Use indented code blocks");
3857
3858 let mdg_ctx = LintContext::new(indented_majority, crate::config::MarkdownFlavor::MDG, None);
3859 let mdg_warnings = rule.check(&mdg_ctx).unwrap();
3860 assert_eq!(mdg_warnings.len(), 2);
3861 assert!(
3862 mdg_warnings
3863 .iter()
3864 .all(|warning| warning.message == "Use fenced code blocks")
3865 );
3866 }
3867
3868 #[test]
3869 fn test_mdg_repairs_unclosed_fence_like_standard() {
3870 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3873 let content = "# Feature: Payloads\n\n```json\n{\"ok\": true}\n";
3874
3875 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3876 let warnings = rule.check(&mdg_ctx).unwrap();
3877 assert_eq!(warnings.len(), 1);
3878 assert!(warnings[0].message.contains("never closed"));
3879
3880 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3881 assert_eq!(
3882 rule.fix(&mdg_ctx).unwrap(),
3883 rule.fix(&standard_ctx).unwrap(),
3884 "MDG must not differ from Standard"
3885 );
3886 }
3887
3888 #[test]
3889 fn test_mdg_table_above_prose_is_never_fenced() {
3890 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3896 let content = "# Feature: Eating\n\n#### Examples:\n\n | start | eat | left |\n | ----- | --- | ---- |\n\n a note about the data\n\n## Scenario: Other\n\n unrelated indented code\n";
3897
3898 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3899 let reported: Vec<usize> = rule.check(&mdg_ctx).unwrap().iter().map(|w| w.line).collect();
3900 assert_eq!(reported, vec![8, 12]);
3901
3902 let fixed = rule.fix(&mdg_ctx).unwrap();
3903 assert_eq!(
3904 fixed,
3905 "# Feature: Eating\n\n#### Examples:\n\n | start | eat | left |\n | ----- | --- | ---- |\n\n```\na note about the data\n```\n\n## Scenario: Other\n\n```\n unrelated indented code\n```\n"
3906 );
3907
3908 let fixed_ctx = LintContext::new(&fixed, crate::config::MarkdownFlavor::MDG, None);
3909 assert!(
3910 rule.check(&fixed_ctx).unwrap().is_empty(),
3911 "MDG check must have nothing left to report after its own fix"
3912 );
3913 assert_eq!(rule.fix(&fixed_ctx).unwrap(), fixed, "MDG fix should be idempotent");
3914
3915 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3917 let standard_reported: Vec<usize> = rule.check(&standard_ctx).unwrap().iter().map(|w| w.line).collect();
3918 assert_eq!(standard_reported, vec![5, 12]);
3919 assert!(rule.fix(&standard_ctx).unwrap().contains("```\n| start | eat | left |"));
3920 }
3921
3922 #[test]
3923 fn test_mdg_repairs_unclosed_fence_under_indented_style() {
3924 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
3928 let content = "# Feature: Payloads\n\n```json\n{\"ok\": true}\n";
3929
3930 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
3931 let warnings = rule.check(&mdg_ctx).unwrap();
3932 assert_eq!(warnings.len(), 1);
3933 assert!(warnings[0].message.contains("never closed"));
3934
3935 let fixed = rule.fix(&mdg_ctx).unwrap();
3936 assert_eq!(fixed, "# Feature: Payloads\n\n```json\n{\"ok\": true}\n```\n");
3937
3938 let fixed_ctx = LintContext::new(&fixed, crate::config::MarkdownFlavor::MDG, None);
3939 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
3940
3941 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3944 assert_eq!(rule.fix(&standard_ctx).unwrap(), fixed);
3945 }
3946
3947 #[test]
3948 fn test_mdg_tab_indented_table_is_not_code() {
3949 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
3953 for indent in ["\t\t", " \t"] {
3954 let content = format!(
3955 "# Feature: Eating\n\n#### Examples:\n\n{indent}| start | eat |\n{indent}| ----- | --- |\n\n## Scenario: Other\n\n code here\n"
3956 );
3957
3958 let mdg_ctx = LintContext::new(&content, crate::config::MarkdownFlavor::MDG, None);
3959 let reported: Vec<usize> = rule.check(&mdg_ctx).unwrap().iter().map(|w| w.line).collect();
3960 assert_eq!(reported, vec![10], "tab-indented rows are a table, not code");
3961
3962 let fixed = rule.fix(&mdg_ctx).unwrap();
3963 assert!(
3964 fixed.contains(&format!("{indent}| start | eat |\n{indent}| ----- | --- |")),
3965 "MDG must leave the tab-indented table alone: {fixed:?}"
3966 );
3967
3968 let standard_ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
3969 let standard_reported: Vec<usize> = rule.check(&standard_ctx).unwrap().iter().map(|w| w.line).collect();
3970 assert_eq!(standard_reported, vec![5, 10]);
3971 }
3972 }
3973
3974 #[test]
3975 fn test_from_config_records_whether_style_was_configured() {
3976 use crate::config::Config;
3980 use std::collections::BTreeMap;
3981
3982 let mut values = BTreeMap::new();
3983 values.insert("style".to_string(), toml::Value::String("indented".to_string()));
3984 let mut config = Config::default();
3985 config.rules.insert(
3986 "MD046".to_string(),
3987 crate::config::RuleConfig { severity: None, values },
3988 );
3989
3990 let configured = MD046CodeBlockStyle::from_config(&config);
3991 let configured = configured.as_any().downcast_ref::<MD046CodeBlockStyle>().unwrap();
3992 assert_eq!(configured.config.style, CodeBlockStyle::Indented);
3993 assert!(configured.style_explicit);
3994
3995 let defaulted = MD046CodeBlockStyle::from_config(&Config::default());
3996 let defaulted = defaulted.as_any().downcast_ref::<MD046CodeBlockStyle>().unwrap();
3997 assert!(!defaulted.style_explicit);
3998
3999 let indented = MD046CodeBlockStyle::from_config_struct(MD046Config {
4002 style: CodeBlockStyle::Indented,
4003 });
4004 let content = "# Feature: F\n\nText.\n\n code here\n";
4005 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
4006 assert!(indented.fix(&mdg_ctx).unwrap().contains("```\n code here\n```"));
4007 }
4008
4009 #[test]
4010 fn test_mdg_indented_style_keeps_tables_out_of_code() {
4011 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
4014 let content = "# Feature: Eating\n\n#### Examples:\n\n | start | eat | left |\n | ----- | --- | ---- |\n";
4015
4016 let mdg_ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDG, None);
4017 assert!(rule.check(&mdg_ctx).unwrap().is_empty());
4018 assert_eq!(rule.fix(&mdg_ctx).unwrap(), content);
4019
4020 let standard_ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
4023 assert!(rule.check(&standard_ctx).unwrap().is_empty());
4024 assert_eq!(rule.fix(&standard_ctx).unwrap(), content);
4025 }
4026
4027 #[test]
4031 fn test_style_warnings_carry_their_block_fix() {
4032 let cases = [
4033 (
4034 CodeBlockStyle::Fenced,
4035 "# T\n\n one\n\n still one\n\nText\n\n two\n",
4036 "# T\n\n```\none\n\nstill one\n```\n\nText\n\n```\ntwo\n```\n",
4037 ),
4038 (
4039 CodeBlockStyle::Indented,
4040 "# T\n\n```\none\n```\n\nText\n\n~~~\ntwo\n~~~\n",
4041 "# T\n\n one\n\nText\n\n two\n",
4042 ),
4043 (
4045 CodeBlockStyle::Fenced,
4046 "# T\n\n ```bash\n ls\n ```\n",
4047 "# T\n\n```bash\nls\n```\n",
4048 ),
4049 (CodeBlockStyle::Fenced, "# T\n\n code", "# T\n\n```\ncode\n```"),
4051 ];
4052 for (style, content, expected) in cases {
4053 let rule = MD046CodeBlockStyle::new(style);
4054 for newline in ["\n", "\r\n"] {
4055 let content = content.replace('\n', newline);
4056 let expected = expected.replace('\n', newline);
4057 let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
4058 assert_eq!(rule.fix(&ctx).unwrap(), expected, "{content:?}");
4059 let warnings = rule.check(&ctx).unwrap();
4060 assert!(!warnings.is_empty(), "{content:?}");
4061 for warning in &warnings {
4062 let fix = warning
4063 .fix
4064 .as_ref()
4065 .unwrap_or_else(|| panic!("{content:?}: {warning:?}"));
4066 let fix_line = content[..fix.range.start].matches('\n').count() + 1;
4067 assert_eq!(
4068 fix_line, warning.line,
4069 "a warning's fix starts at its block: {content:?}"
4070 );
4071 }
4072 let edited = crate::utils::fix_utils::apply_warning_fixes(&content, &warnings).unwrap();
4073 assert_eq!(edited, expected, "{content:?}");
4074
4075 let first = crate::utils::fix_utils::apply_warning_fixes(&content, &warnings[..1]).unwrap();
4077 let first_ctx = LintContext::new(&first, crate::config::MarkdownFlavor::Standard, None);
4078 assert_eq!(
4079 rule.check(&first_ctx).unwrap().len(),
4080 warnings.len() - 1,
4081 "{content:?} -> {first:?}"
4082 );
4083 }
4084 }
4085 }
4086
4087 #[test]
4089 fn test_style_warning_without_a_fix_where_fix_keeps_the_block() {
4090 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
4091 let content = "# T\n\n ```\n code\n";
4094 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
4095 assert_eq!(rule.fix(&ctx).unwrap(), content);
4096 let warnings = rule.check(&ctx).unwrap();
4097 assert_eq!(warnings.len(), 1, "{warnings:?}");
4098 assert!(warnings[0].fix.is_none(), "{warnings:?}");
4099 }
4100}