1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::rules::code_block_utils::CodeBlockStyle;
3use crate::utils::mkdocs_tabs;
4use crate::utils::range_utils::{LineIndex, calculate_line_range};
5use toml;
6
7mod md046_config;
8use md046_config::MD046Config;
9
10#[derive(Clone)]
16pub struct MD046CodeBlockStyle {
17 config: MD046Config,
18}
19
20impl MD046CodeBlockStyle {
21 pub fn new(style: CodeBlockStyle) -> Self {
22 Self {
23 config: MD046Config { style },
24 }
25 }
26
27 pub fn from_config_struct(config: MD046Config) -> Self {
28 Self { config }
29 }
30
31 fn is_fenced_code_block_start(&self, line: &str) -> bool {
32 let trimmed = line.trim_start();
33 trimmed.starts_with("```") || trimmed.starts_with("~~~")
34 }
35
36 fn is_list_item(&self, line: &str) -> bool {
37 let trimmed = line.trim_start();
38 (trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ "))
39 || (trimmed.len() > 2
40 && trimmed.chars().next().unwrap().is_numeric()
41 && (trimmed.contains(". ") || trimmed.contains(") ")))
42 }
43
44 fn precompute_list_context(&self, lines: &[&str]) -> Vec<bool> {
47 let mut in_list_context = vec![false; lines.len()];
48 let mut last_list_item_line: Option<usize> = None;
49
50 for (i, line) in lines.iter().enumerate() {
51 let trimmed = line.trim_start();
52
53 if self.is_list_item(line) {
55 last_list_item_line = Some(i);
56 in_list_context[i] = true;
57 continue;
58 }
59
60 if line.trim().is_empty() {
62 if last_list_item_line.is_some() {
63 in_list_context[i] = true;
64 }
65 continue;
66 }
67
68 let indent_len = line.len() - trimmed.len();
70 if indent_len == 0 && !trimmed.is_empty() {
71 if trimmed.starts_with('#') {
73 last_list_item_line = None;
74 continue;
75 }
76 if trimmed.starts_with("---") || trimmed.starts_with("***") {
78 last_list_item_line = None;
79 continue;
80 }
81 if let Some(list_line) = last_list_item_line
83 && i - list_line > 5
84 {
85 last_list_item_line = None;
86 continue;
87 }
88 }
89
90 if last_list_item_line.is_some() {
92 in_list_context[i] = true;
93 }
94 }
95
96 in_list_context
97 }
98
99 fn is_indented_code_block_with_context(
101 &self,
102 lines: &[&str],
103 i: usize,
104 is_mkdocs: bool,
105 in_list_context: &[bool],
106 in_tab_context: &[bool],
107 ) -> bool {
108 if i >= lines.len() {
109 return false;
110 }
111
112 let line = lines[i];
113
114 if !(line.starts_with(" ") || line.starts_with("\t")) {
116 return false;
117 }
118
119 if in_list_context[i] {
121 return false;
122 }
123
124 if is_mkdocs && in_tab_context[i] {
126 return false;
127 }
128
129 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
132 let prev_is_indented_code = i > 0
133 && (lines[i - 1].starts_with(" ") || lines[i - 1].starts_with("\t"))
134 && !in_list_context[i - 1]
135 && !(is_mkdocs && in_tab_context[i - 1]);
136
137 if !has_blank_line_before && !prev_is_indented_code {
140 return false;
141 }
142
143 true
144 }
145
146 fn precompute_mkdocs_tab_context(&self, lines: &[&str]) -> Vec<bool> {
148 let mut in_tab_context = vec![false; lines.len()];
149 let mut current_tab_indent: Option<usize> = None;
150
151 for (i, line) in lines.iter().enumerate() {
152 if mkdocs_tabs::is_tab_marker(line) {
154 let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
155 current_tab_indent = Some(tab_indent);
156 in_tab_context[i] = true;
157 continue;
158 }
159
160 if let Some(tab_indent) = current_tab_indent {
162 if mkdocs_tabs::is_tab_content(line, tab_indent) {
163 in_tab_context[i] = true;
164 } else if !line.trim().is_empty() && !line.starts_with(" ") {
165 current_tab_indent = None;
167 } else {
168 in_tab_context[i] = true;
170 }
171 }
172 }
173
174 in_tab_context
175 }
176
177 fn check_unclosed_code_blocks(
178 &self,
179 ctx: &crate::lint_context::LintContext,
180 line_index: &LineIndex,
181 ) -> Result<Vec<LintWarning>, LintError> {
182 let mut warnings = Vec::new();
183 let lines: Vec<&str> = ctx.content.lines().collect();
184 let mut fence_stack: Vec<(String, usize, usize, bool, bool)> = Vec::new(); let mut inside_markdown_documentation_block = false;
189
190 for (i, line) in lines.iter().enumerate() {
191 let trimmed = line.trim_start();
192
193 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
195 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
196
197 let fence_length = trimmed.chars().take_while(|&c| c == fence_char).count();
199
200 let after_fence = &trimmed[fence_length..];
202
203 let is_valid_fence_pattern = if after_fence.is_empty() {
209 true
211 } else if after_fence.starts_with(' ') || after_fence.starts_with('\t') {
212 true
214 } else {
215 let identifier = after_fence.trim().to_lowercase();
218
219 if identifier.contains("fence") || identifier.contains("still") {
221 false
222 } else if identifier.len() > 20 {
223 false
225 } else if let Some(first_char) = identifier.chars().next() {
226 if !first_char.is_alphabetic() && first_char != '#' {
228 false
229 } else {
230 let valid_chars = identifier.chars().all(|c| {
233 c.is_alphanumeric() || c == '-' || c == '_' || c == '+' || c == '#' || c == '.'
234 });
235
236 valid_chars && identifier.len() >= 2
238 }
239 } else {
240 false
241 }
242 };
243
244 if !fence_stack.is_empty() {
246 if !is_valid_fence_pattern {
248 continue;
249 }
250
251 if let Some((open_marker, open_length, _, _, _)) = fence_stack.last() {
253 if fence_char == open_marker.chars().next().unwrap() && fence_length >= *open_length {
254 if !after_fence.trim().is_empty() {
256 let has_special_chars = after_fence.chars().any(|c| {
262 !c.is_alphanumeric()
263 && c != '-'
264 && c != '_'
265 && c != '+'
266 && c != '#'
267 && c != '.'
268 && c != ' '
269 && c != '\t'
270 });
271
272 if has_special_chars {
273 continue; }
275
276 if fence_length > 4 && after_fence.chars().take(4).all(|c| !c.is_alphanumeric()) {
278 continue; }
280
281 if !after_fence.starts_with(' ') && !after_fence.starts_with('\t') {
283 let identifier = after_fence.trim();
284
285 if let Some(first) = identifier.chars().next()
287 && !first.is_alphabetic()
288 && first != '#'
289 {
290 continue;
291 }
292
293 if identifier.len() > 30 {
295 continue;
296 }
297 }
298 }
299 } else {
301 if !after_fence.is_empty()
306 && !after_fence.starts_with(' ')
307 && !after_fence.starts_with('\t')
308 {
309 let identifier = after_fence.trim();
311
312 if identifier.chars().any(|c| {
314 !c.is_alphanumeric() && c != '-' && c != '_' && c != '+' && c != '#' && c != '.'
315 }) {
316 continue;
317 }
318
319 if let Some(first) = identifier.chars().next()
321 && !first.is_alphabetic()
322 && first != '#'
323 {
324 continue;
325 }
326 }
327 }
328 }
329 }
330
331 if let Some((open_marker, open_length, _open_line, _flagged, _is_md)) = fence_stack.last() {
335 if fence_char == open_marker.chars().next().unwrap() && fence_length >= *open_length {
337 let after_fence = &trimmed[fence_length..];
339 if after_fence.trim().is_empty() {
340 let _popped = fence_stack.pop();
342
343 if let Some((_, _, _, _, is_md)) = _popped
345 && is_md
346 {
347 inside_markdown_documentation_block = false;
348 }
349 continue;
350 }
351 }
352 }
353
354 if !after_fence.trim().is_empty() || fence_stack.is_empty() {
357 let has_nested_issue =
361 if let Some((open_marker, open_length, open_line, _, _)) = fence_stack.last_mut() {
362 if fence_char == open_marker.chars().next().unwrap()
363 && fence_length >= *open_length
364 && !inside_markdown_documentation_block
365 {
366 let (opening_start_line, opening_start_col, opening_end_line, opening_end_col) =
368 calculate_line_range(*open_line, lines[*open_line - 1]);
369
370 let line_start_byte = line_index.get_line_start_byte(i + 1).unwrap_or(0);
372
373 warnings.push(LintWarning {
374 rule_name: Some(self.name().to_string()),
375 line: opening_start_line,
376 column: opening_start_col,
377 end_line: opening_end_line,
378 end_column: opening_end_col,
379 message: format!(
380 "Code block '{}' should be closed before starting new one at line {}",
381 open_marker,
382 i + 1
383 ),
384 severity: Severity::Warning,
385 fix: Some(Fix {
386 range: (line_start_byte..line_start_byte),
387 replacement: format!("{open_marker}\n\n"),
388 }),
389 });
390
391 fence_stack.last_mut().unwrap().3 = true;
393 true } else {
395 false
396 }
397 } else {
398 false
399 };
400
401 let after_fence_for_lang = &trimmed[fence_length..];
403 let lang_info = after_fence_for_lang.trim().to_lowercase();
404 let is_markdown_fence = lang_info.starts_with("markdown") || lang_info.starts_with("md");
405
406 if is_markdown_fence && !inside_markdown_documentation_block {
408 inside_markdown_documentation_block = true;
409 }
410
411 let fence_marker = fence_char.to_string().repeat(fence_length);
413 fence_stack.push((fence_marker, fence_length, i + 1, has_nested_issue, is_markdown_fence));
414 }
415 }
416 }
417
418 for (fence_marker, _, opening_line, flagged_for_nested, _) in fence_stack {
421 if !flagged_for_nested {
422 let (start_line, start_col, end_line, end_col) =
423 calculate_line_range(opening_line, lines[opening_line - 1]);
424
425 warnings.push(LintWarning {
426 rule_name: Some(self.name().to_string()),
427 line: start_line,
428 column: start_col,
429 end_line,
430 end_column: end_col,
431 message: format!("Code block opened with '{fence_marker}' but never closed"),
432 severity: Severity::Warning,
433 fix: Some(Fix {
434 range: (ctx.content.len()..ctx.content.len()),
435 replacement: format!("\n{fence_marker}"),
436 }),
437 });
438 }
439 }
440
441 Ok(warnings)
442 }
443
444 fn detect_style(&self, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
445 if content.is_empty() {
447 return None;
448 }
449
450 let lines: Vec<&str> = content.lines().collect();
451 let mut fenced_found = false;
452 let mut indented_found = false;
453 let mut fenced_line = usize::MAX;
454 let mut indented_line = usize::MAX;
455
456 let in_list_context = self.precompute_list_context(&lines);
458 let in_tab_context = if is_mkdocs {
459 self.precompute_mkdocs_tab_context(&lines)
460 } else {
461 vec![false; lines.len()]
462 };
463
464 for (i, line) in lines.iter().enumerate() {
466 if self.is_fenced_code_block_start(line) {
467 fenced_found = true;
468 fenced_line = fenced_line.min(i);
469 } else if self.is_indented_code_block_with_context(&lines, i, is_mkdocs, &in_list_context, &in_tab_context)
470 {
471 indented_found = true;
472 indented_line = indented_line.min(i);
473 }
474 }
475
476 if !fenced_found && !indented_found {
477 None
479 } else if fenced_found && !indented_found {
480 Some(CodeBlockStyle::Fenced)
482 } else if !fenced_found && indented_found {
483 Some(CodeBlockStyle::Indented)
485 } else {
486 if indented_line < fenced_line {
488 Some(CodeBlockStyle::Indented)
489 } else {
490 Some(CodeBlockStyle::Fenced)
491 }
492 }
493 }
494}
495
496impl Rule for MD046CodeBlockStyle {
497 fn name(&self) -> &'static str {
498 "MD046"
499 }
500
501 fn description(&self) -> &'static str {
502 "Code blocks should use a consistent style"
503 }
504
505 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
506 if ctx.content.is_empty() {
508 return Ok(Vec::new());
509 }
510
511 if !ctx.content.contains("```") && !ctx.content.contains("~~~") && !ctx.content.contains(" ") {
513 return Ok(Vec::new());
514 }
515
516 let line_index = LineIndex::new(ctx.content.to_string());
518 let unclosed_warnings = self.check_unclosed_code_blocks(ctx, &line_index)?;
519
520 if !unclosed_warnings.is_empty() {
522 return Ok(unclosed_warnings);
523 }
524
525 let lines: Vec<&str> = ctx.content.lines().collect();
527 let mut warnings = Vec::new();
528
529 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
531
532 let in_list_context = self.precompute_list_context(&lines);
534 let in_tab_context = if is_mkdocs {
535 self.precompute_mkdocs_tab_context(&lines)
536 } else {
537 vec![false; lines.len()]
538 };
539
540 let target_style = match self.config.style {
542 CodeBlockStyle::Consistent => self
543 .detect_style(ctx.content, is_mkdocs)
544 .unwrap_or(CodeBlockStyle::Fenced),
545 _ => self.config.style,
546 };
547
548 let line_index = LineIndex::new(ctx.content.to_string());
550
551 let mut in_fenced_block = vec![false; lines.len()];
554 for &(start, end) in &ctx.code_blocks {
555 if start < ctx.content.len() && end <= ctx.content.len() {
557 let block_content = &ctx.content[start..end];
558 let is_fenced = block_content.starts_with("```") || block_content.starts_with("~~~");
559
560 if is_fenced {
561 for (line_idx, line_info) in ctx.lines.iter().enumerate() {
563 if line_info.byte_offset >= start && line_info.byte_offset < end {
564 in_fenced_block[line_idx] = true;
565 }
566 }
567 }
568 }
569 }
570
571 let mut in_fence = false;
572 for (i, line) in lines.iter().enumerate() {
573 let trimmed = line.trim_start();
574
575 if ctx.line_info(i + 1).is_some_and(|info| info.in_html_block) {
577 continue;
578 }
579
580 if ctx.lines[i].in_mkdocstrings {
583 continue;
584 }
585
586 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
588 if target_style == CodeBlockStyle::Indented && !in_fence {
589 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
592 warnings.push(LintWarning {
593 rule_name: Some(self.name().to_string()),
594 line: start_line,
595 column: start_col,
596 end_line,
597 end_column: end_col,
598 message: "Use indented code blocks".to_string(),
599 severity: Severity::Warning,
600 fix: Some(Fix {
601 range: line_index.line_col_to_byte_range(i + 1, 1),
602 replacement: String::new(),
603 }),
604 });
605 }
606 in_fence = !in_fence;
608 continue;
609 }
610
611 if in_fenced_block[i] {
614 continue;
615 }
616
617 if self.is_indented_code_block_with_context(&lines, i, is_mkdocs, &in_list_context, &in_tab_context)
619 && target_style == CodeBlockStyle::Fenced
620 {
621 let prev_line_is_indented = i > 0
623 && self.is_indented_code_block_with_context(
624 &lines,
625 i - 1,
626 is_mkdocs,
627 &in_list_context,
628 &in_tab_context,
629 );
630
631 if !prev_line_is_indented {
632 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
633 warnings.push(LintWarning {
634 rule_name: Some(self.name().to_string()),
635 line: start_line,
636 column: start_col,
637 end_line,
638 end_column: end_col,
639 message: "Use fenced code blocks".to_string(),
640 severity: Severity::Warning,
641 fix: Some(Fix {
642 range: line_index.line_col_to_byte_range(i + 1, 1),
643 replacement: format!("```\n{}", line.trim_start()),
644 }),
645 });
646 }
647 }
648 }
649
650 Ok(warnings)
651 }
652
653 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
654 let content = ctx.content;
655 if content.is_empty() {
656 return Ok(String::new());
657 }
658
659 let line_index = LineIndex::new(ctx.content.to_string());
661 let unclosed_warnings = self.check_unclosed_code_blocks(ctx, &line_index)?;
662
663 if !unclosed_warnings.is_empty() {
665 for warning in &unclosed_warnings {
667 if warning
668 .message
669 .contains("should be closed before starting new one at line")
670 {
671 if let Some(fix) = &warning.fix {
673 let mut result = String::new();
674 result.push_str(&content[..fix.range.start]);
675 result.push_str(&fix.replacement);
676 result.push_str(&content[fix.range.start..]);
677 return Ok(result);
678 }
679 }
680 }
681 }
682
683 let lines: Vec<&str> = content.lines().collect();
684
685 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
687 let target_style = match self.config.style {
688 CodeBlockStyle::Consistent => self.detect_style(content, is_mkdocs).unwrap_or(CodeBlockStyle::Fenced),
689 _ => self.config.style,
690 };
691
692 let in_list_context = self.precompute_list_context(&lines);
694 let in_tab_context = if is_mkdocs {
695 self.precompute_mkdocs_tab_context(&lines)
696 } else {
697 vec![false; lines.len()]
698 };
699
700 let mut result = String::with_capacity(content.len());
701 let mut in_fenced_block = false;
702 let mut fenced_fence_type = None;
703 let mut in_indented_block = false;
704
705 for (i, line) in lines.iter().enumerate() {
706 let trimmed = line.trim_start();
707
708 if !in_fenced_block && (trimmed.starts_with("```") || trimmed.starts_with("~~~")) {
710 in_fenced_block = true;
711 fenced_fence_type = Some(if trimmed.starts_with("```") { "```" } else { "~~~" });
712
713 if target_style == CodeBlockStyle::Indented {
714 in_indented_block = true;
716 } else {
717 result.push_str(line);
719 result.push('\n');
720 }
721 } else if in_fenced_block && fenced_fence_type.is_some() {
722 let fence = fenced_fence_type.unwrap();
723 if trimmed.starts_with(fence) {
724 in_fenced_block = false;
725 fenced_fence_type = None;
726 in_indented_block = false;
727
728 if target_style == CodeBlockStyle::Indented {
729 } else {
731 result.push_str(line);
733 result.push('\n');
734 }
735 } else if target_style == CodeBlockStyle::Indented {
736 result.push_str(" ");
738 result.push_str(trimmed);
739 result.push('\n');
740 } else {
741 result.push_str(line);
743 result.push('\n');
744 }
745 } else if self.is_indented_code_block_with_context(&lines, i, is_mkdocs, &in_list_context, &in_tab_context)
746 {
747 let prev_line_is_indented = i > 0
751 && self.is_indented_code_block_with_context(
752 &lines,
753 i - 1,
754 is_mkdocs,
755 &in_list_context,
756 &in_tab_context,
757 );
758
759 if target_style == CodeBlockStyle::Fenced {
760 if !prev_line_is_indented && !in_indented_block {
761 result.push_str("```\n");
763 result.push_str(line.trim_start());
764 result.push('\n');
765 in_indented_block = true;
766 } else {
767 result.push_str(line.trim_start());
769 result.push('\n');
770 }
771
772 let _next_line_is_indented = i < lines.len() - 1
774 && self.is_indented_code_block_with_context(
775 &lines,
776 i + 1,
777 is_mkdocs,
778 &in_list_context,
779 &in_tab_context,
780 );
781 if !_next_line_is_indented && in_indented_block {
782 result.push_str("```\n");
783 in_indented_block = false;
784 }
785 } else {
786 result.push_str(line);
788 result.push('\n');
789 }
790 } else {
791 if in_indented_block && target_style == CodeBlockStyle::Fenced {
793 result.push_str("```\n");
794 in_indented_block = false;
795 }
796
797 result.push_str(line);
798 result.push('\n');
799 }
800 }
801
802 if in_indented_block && target_style == CodeBlockStyle::Fenced {
804 result.push_str("```\n");
805 }
806
807 if let Some(fence_type) = fenced_fence_type
809 && in_fenced_block
810 {
811 result.push_str(fence_type);
812 result.push('\n');
813 }
814
815 if !content.ends_with('\n') && result.ends_with('\n') {
817 result.pop();
818 }
819
820 Ok(result)
821 }
822
823 fn category(&self) -> RuleCategory {
825 RuleCategory::CodeBlock
826 }
827
828 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
830 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
833 }
834
835 fn as_any(&self) -> &dyn std::any::Any {
836 self
837 }
838
839 fn default_config_section(&self) -> Option<(String, toml::Value)> {
840 let json_value = serde_json::to_value(&self.config).ok()?;
841 Some((
842 self.name().to_string(),
843 crate::rule_config_serde::json_to_toml_value(&json_value)?,
844 ))
845 }
846
847 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
848 where
849 Self: Sized,
850 {
851 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
852 Box::new(Self::from_config_struct(rule_config))
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859 use crate::lint_context::LintContext;
860
861 #[test]
862 fn test_fenced_code_block_detection() {
863 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
864 assert!(rule.is_fenced_code_block_start("```"));
865 assert!(rule.is_fenced_code_block_start("```rust"));
866 assert!(rule.is_fenced_code_block_start("~~~"));
867 assert!(rule.is_fenced_code_block_start("~~~python"));
868 assert!(rule.is_fenced_code_block_start(" ```"));
869 assert!(!rule.is_fenced_code_block_start("``"));
870 assert!(!rule.is_fenced_code_block_start("~~"));
871 assert!(!rule.is_fenced_code_block_start("Regular text"));
872 }
873
874 #[test]
875 fn test_consistent_style_with_fenced_blocks() {
876 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
877 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
878 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
879 let result = rule.check(&ctx).unwrap();
880
881 assert_eq!(result.len(), 0);
883 }
884
885 #[test]
886 fn test_consistent_style_with_indented_blocks() {
887 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
888 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
889 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
890 let result = rule.check(&ctx).unwrap();
891
892 assert_eq!(result.len(), 0);
894 }
895
896 #[test]
897 fn test_consistent_style_mixed() {
898 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
899 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
900 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
901 let result = rule.check(&ctx).unwrap();
902
903 assert!(!result.is_empty());
905 }
906
907 #[test]
908 fn test_fenced_style_with_indented_blocks() {
909 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
910 let content = "Text\n\n indented code\n more code\n\nMore text";
911 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
912 let result = rule.check(&ctx).unwrap();
913
914 assert!(!result.is_empty());
916 assert!(result[0].message.contains("Use fenced code blocks"));
917 }
918
919 #[test]
920 fn test_indented_style_with_fenced_blocks() {
921 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
922 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
923 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
924 let result = rule.check(&ctx).unwrap();
925
926 assert!(!result.is_empty());
928 assert!(result[0].message.contains("Use indented code blocks"));
929 }
930
931 #[test]
932 fn test_unclosed_code_block() {
933 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
934 let content = "```\ncode without closing fence";
935 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
936 let result = rule.check(&ctx).unwrap();
937
938 assert_eq!(result.len(), 1);
939 assert!(result[0].message.contains("never closed"));
940 }
941
942 #[test]
943 fn test_nested_code_blocks() {
944 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
945 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
946 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
947 let result = rule.check(&ctx).unwrap();
948
949 assert_eq!(result.len(), 0);
951 }
952
953 #[test]
954 fn test_fix_indented_to_fenced() {
955 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
956 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
957 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
958 let fixed = rule.fix(&ctx).unwrap();
959
960 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
961 }
962
963 #[test]
964 fn test_fix_fenced_to_indented() {
965 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
966 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
967 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
968 let fixed = rule.fix(&ctx).unwrap();
969
970 assert!(fixed.contains(" code line 1\n code line 2"));
971 assert!(!fixed.contains("```"));
972 }
973
974 #[test]
975 fn test_fix_unclosed_block() {
976 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
977 let content = "```\ncode without closing";
978 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
979 let fixed = rule.fix(&ctx).unwrap();
980
981 assert!(fixed.ends_with("```"));
983 }
984
985 #[test]
986 fn test_code_block_in_list() {
987 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
988 let content = "- List item\n code in list\n more code\n- Next item";
989 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
990 let result = rule.check(&ctx).unwrap();
991
992 assert_eq!(result.len(), 0);
994 }
995
996 #[test]
997 fn test_detect_style_fenced() {
998 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
999 let content = "```\ncode\n```";
1000 let style = rule.detect_style(content, false);
1001
1002 assert_eq!(style, Some(CodeBlockStyle::Fenced));
1003 }
1004
1005 #[test]
1006 fn test_detect_style_indented() {
1007 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1008 let content = "Text\n\n code\n\nMore";
1009 let style = rule.detect_style(content, false);
1010
1011 assert_eq!(style, Some(CodeBlockStyle::Indented));
1012 }
1013
1014 #[test]
1015 fn test_detect_style_none() {
1016 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1017 let content = "No code blocks here";
1018 let style = rule.detect_style(content, false);
1019
1020 assert_eq!(style, None);
1021 }
1022
1023 #[test]
1024 fn test_tilde_fence() {
1025 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1026 let content = "~~~\ncode\n~~~";
1027 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1028 let result = rule.check(&ctx).unwrap();
1029
1030 assert_eq!(result.len(), 0);
1032 }
1033
1034 #[test]
1035 fn test_language_specification() {
1036 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1037 let content = "```rust\nfn main() {}\n```";
1038 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1039 let result = rule.check(&ctx).unwrap();
1040
1041 assert_eq!(result.len(), 0);
1042 }
1043
1044 #[test]
1045 fn test_empty_content() {
1046 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1047 let content = "";
1048 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1049 let result = rule.check(&ctx).unwrap();
1050
1051 assert_eq!(result.len(), 0);
1052 }
1053
1054 #[test]
1055 fn test_default_config() {
1056 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1057 let (name, _config) = rule.default_config_section().unwrap();
1058 assert_eq!(name, "MD046");
1059 }
1060
1061 #[test]
1062 fn test_markdown_documentation_block() {
1063 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1064 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
1065 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1066 let result = rule.check(&ctx).unwrap();
1067
1068 assert_eq!(result.len(), 0);
1070 }
1071
1072 #[test]
1073 fn test_preserve_trailing_newline() {
1074 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1075 let content = "```\ncode\n```\n";
1076 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1077 let fixed = rule.fix(&ctx).unwrap();
1078
1079 assert_eq!(fixed, content);
1080 }
1081
1082 #[test]
1083 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
1084 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1085 let content = r#"# Document
1086
1087=== "Python"
1088
1089 This is tab content
1090 Not an indented code block
1091
1092 ```python
1093 def hello():
1094 print("Hello")
1095 ```
1096
1097=== "JavaScript"
1098
1099 More tab content here
1100 Also not an indented code block"#;
1101
1102 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs);
1103 let result = rule.check(&ctx).unwrap();
1104
1105 assert_eq!(result.len(), 0);
1107 }
1108
1109 #[test]
1110 fn test_mkdocs_tabs_with_actual_indented_code() {
1111 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1112 let content = r#"# Document
1113
1114=== "Tab 1"
1115
1116 This is tab content
1117
1118Regular text
1119
1120 This is an actual indented code block
1121 Should be flagged"#;
1122
1123 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs);
1124 let result = rule.check(&ctx).unwrap();
1125
1126 assert_eq!(result.len(), 1);
1128 assert!(result[0].message.contains("Use fenced code blocks"));
1129 }
1130
1131 #[test]
1132 fn test_mkdocs_tabs_detect_style() {
1133 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1134 let content = r#"=== "Tab 1"
1135
1136 Content in tab
1137 More content
1138
1139=== "Tab 2"
1140
1141 Content in second tab"#;
1142
1143 let style = rule.detect_style(content, true);
1145 assert_eq!(style, None); let style = rule.detect_style(content, false);
1149 assert_eq!(style, Some(CodeBlockStyle::Indented));
1150 }
1151
1152 #[test]
1153 fn test_mkdocs_nested_tabs() {
1154 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1155 let content = r#"# Document
1156
1157=== "Outer Tab"
1158
1159 Some content
1160
1161 === "Nested Tab"
1162
1163 Nested tab content
1164 Should not be flagged"#;
1165
1166 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs);
1167 let result = rule.check(&ctx).unwrap();
1168
1169 assert_eq!(result.len(), 0);
1171 }
1172}