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 is_indented_code_block(&self, lines: &[&str], i: usize, is_mkdocs: bool) -> bool {
45 if i >= lines.len() {
46 return false;
47 }
48
49 let line = lines[i];
50
51 if !(line.starts_with(" ") || line.starts_with("\t")) {
53 return false;
54 }
55
56 if self.is_part_of_list_structure(lines, i) {
58 return false;
59 }
60
61 if is_mkdocs && self.is_in_mkdocs_tab(lines, i) {
63 return false;
64 }
65
66 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
69 let prev_is_indented_code = i > 0
70 && (lines[i - 1].starts_with(" ") || lines[i - 1].starts_with("\t"))
71 && !self.is_part_of_list_structure(lines, i - 1)
72 && !(is_mkdocs && self.is_in_mkdocs_tab(lines, i - 1));
73
74 if !has_blank_line_before && !prev_is_indented_code {
77 return false;
78 }
79
80 true
81 }
82
83 fn is_part_of_list_structure(&self, lines: &[&str], i: usize) -> bool {
85 for j in (0..i).rev() {
89 let line = lines[j];
90
91 if line.trim().is_empty() {
93 continue;
94 }
95
96 if self.is_list_item(line) {
98 return true;
99 }
100
101 let trimmed = line.trim_start();
104 let indent_len = line.len() - trimmed.len();
105
106 if indent_len == 0 && !trimmed.is_empty() {
109 if trimmed.starts_with('#') {
111 break;
112 }
113 if trimmed.starts_with("---") || trimmed.starts_with("***") {
115 break;
116 }
117 if j > 0 && i >= 5 && j < i - 5 {
121 break;
123 }
124 }
125
126 }
128
129 false
130 }
131
132 fn is_in_mkdocs_tab(&self, lines: &[&str], i: usize) -> bool {
134 for j in (0..i).rev() {
136 let line = lines[j];
137
138 if mkdocs_tabs::is_tab_marker(line) {
140 let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
141 if mkdocs_tabs::is_tab_content(lines[i], tab_indent) {
143 return true;
144 }
145 return false;
147 }
148
149 if !line.trim().is_empty() && !line.starts_with(" ") && !mkdocs_tabs::is_tab_marker(line) {
151 break;
152 }
153 }
154 false
155 }
156
157 fn check_unclosed_code_blocks(
158 &self,
159 ctx: &crate::lint_context::LintContext,
160 line_index: &LineIndex,
161 ) -> Result<Vec<LintWarning>, LintError> {
162 let mut warnings = Vec::new();
163 let lines: Vec<&str> = ctx.content.lines().collect();
164 let mut fence_stack: Vec<(String, usize, usize, bool, bool)> = Vec::new(); let mut inside_markdown_documentation_block = false;
169
170 for (i, line) in lines.iter().enumerate() {
171 let trimmed = line.trim_start();
172
173 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
175 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
176
177 let fence_length = trimmed.chars().take_while(|&c| c == fence_char).count();
179
180 let after_fence = &trimmed[fence_length..];
182
183 let is_valid_fence_pattern = if after_fence.is_empty() {
189 true
191 } else if after_fence.starts_with(' ') || after_fence.starts_with('\t') {
192 true
194 } else {
195 let identifier = after_fence.trim().to_lowercase();
198
199 if identifier.contains("fence") || identifier.contains("still") {
201 false
202 } else if identifier.len() > 20 {
203 false
205 } else if let Some(first_char) = identifier.chars().next() {
206 if !first_char.is_alphabetic() && first_char != '#' {
208 false
209 } else {
210 let valid_chars = identifier.chars().all(|c| {
213 c.is_alphanumeric() || c == '-' || c == '_' || c == '+' || c == '#' || c == '.'
214 });
215
216 valid_chars && identifier.len() >= 2
218 }
219 } else {
220 false
221 }
222 };
223
224 if !fence_stack.is_empty() {
226 if !is_valid_fence_pattern {
228 continue;
229 }
230
231 if let Some((open_marker, open_length, _, _, _)) = fence_stack.last() {
233 if fence_char == open_marker.chars().next().unwrap() && fence_length >= *open_length {
234 if !after_fence.trim().is_empty() {
236 let has_special_chars = after_fence.chars().any(|c| {
242 !c.is_alphanumeric()
243 && c != '-'
244 && c != '_'
245 && c != '+'
246 && c != '#'
247 && c != '.'
248 && c != ' '
249 && c != '\t'
250 });
251
252 if has_special_chars {
253 continue; }
255
256 if fence_length > 4 && after_fence.chars().take(4).all(|c| !c.is_alphanumeric()) {
258 continue; }
260
261 if !after_fence.starts_with(' ') && !after_fence.starts_with('\t') {
263 let identifier = after_fence.trim();
264
265 if let Some(first) = identifier.chars().next()
267 && !first.is_alphabetic()
268 && first != '#'
269 {
270 continue;
271 }
272
273 if identifier.len() > 30 {
275 continue;
276 }
277 }
278 }
279 } else {
281 if !after_fence.is_empty()
286 && !after_fence.starts_with(' ')
287 && !after_fence.starts_with('\t')
288 {
289 let identifier = after_fence.trim();
291
292 if identifier.chars().any(|c| {
294 !c.is_alphanumeric() && c != '-' && c != '_' && c != '+' && c != '#' && c != '.'
295 }) {
296 continue;
297 }
298
299 if let Some(first) = identifier.chars().next()
301 && !first.is_alphabetic()
302 && first != '#'
303 {
304 continue;
305 }
306 }
307 }
308 }
309 }
310
311 if let Some((open_marker, open_length, _open_line, _flagged, _is_md)) = fence_stack.last() {
315 if fence_char == open_marker.chars().next().unwrap() && fence_length >= *open_length {
317 let after_fence = &trimmed[fence_length..];
319 if after_fence.trim().is_empty() {
320 let _popped = fence_stack.pop();
322
323 if let Some((_, _, _, _, is_md)) = _popped
325 && is_md
326 {
327 inside_markdown_documentation_block = false;
328 }
329 continue;
330 }
331 }
332 }
333
334 if !after_fence.trim().is_empty() || fence_stack.is_empty() {
337 let has_nested_issue =
341 if let Some((open_marker, open_length, open_line, _, _)) = fence_stack.last_mut() {
342 if fence_char == open_marker.chars().next().unwrap()
343 && fence_length >= *open_length
344 && !inside_markdown_documentation_block
345 {
346 let (opening_start_line, opening_start_col, opening_end_line, opening_end_col) =
348 calculate_line_range(*open_line, lines[*open_line - 1]);
349
350 let line_start_byte = line_index.get_line_start_byte(i + 1).unwrap_or(0);
352
353 warnings.push(LintWarning {
354 rule_name: Some(self.name().to_string()),
355 line: opening_start_line,
356 column: opening_start_col,
357 end_line: opening_end_line,
358 end_column: opening_end_col,
359 message: format!(
360 "Code block '{}' should be closed before starting new one at line {}",
361 open_marker,
362 i + 1
363 ),
364 severity: Severity::Warning,
365 fix: Some(Fix {
366 range: (line_start_byte..line_start_byte),
367 replacement: format!("{open_marker}\n\n"),
368 }),
369 });
370
371 fence_stack.last_mut().unwrap().3 = true;
373 true } else {
375 false
376 }
377 } else {
378 false
379 };
380
381 let after_fence_for_lang = &trimmed[fence_length..];
383 let lang_info = after_fence_for_lang.trim().to_lowercase();
384 let is_markdown_fence = lang_info.starts_with("markdown") || lang_info.starts_with("md");
385
386 if is_markdown_fence && !inside_markdown_documentation_block {
388 inside_markdown_documentation_block = true;
389 }
390
391 let fence_marker = fence_char.to_string().repeat(fence_length);
393 fence_stack.push((fence_marker, fence_length, i + 1, has_nested_issue, is_markdown_fence));
394 }
395 }
396 }
397
398 for (fence_marker, _, opening_line, flagged_for_nested, _) in fence_stack {
401 if !flagged_for_nested {
402 let (start_line, start_col, end_line, end_col) =
403 calculate_line_range(opening_line, lines[opening_line - 1]);
404
405 warnings.push(LintWarning {
406 rule_name: Some(self.name().to_string()),
407 line: start_line,
408 column: start_col,
409 end_line,
410 end_column: end_col,
411 message: format!("Code block opened with '{fence_marker}' but never closed"),
412 severity: Severity::Warning,
413 fix: Some(Fix {
414 range: (ctx.content.len()..ctx.content.len()),
415 replacement: format!("\n{fence_marker}"),
416 }),
417 });
418 }
419 }
420
421 Ok(warnings)
422 }
423
424 fn detect_style(&self, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
425 if content.is_empty() {
427 return None;
428 }
429
430 let lines: Vec<&str> = content.lines().collect();
431 let mut fenced_found = false;
432 let mut indented_found = false;
433 let mut fenced_line = usize::MAX;
434 let mut indented_line = usize::MAX;
435
436 for (i, line) in lines.iter().enumerate() {
438 if self.is_fenced_code_block_start(line) {
439 fenced_found = true;
440 fenced_line = fenced_line.min(i);
441 } else if self.is_indented_code_block(&lines, i, is_mkdocs) {
442 indented_found = true;
443 indented_line = indented_line.min(i);
444 }
445 }
446
447 if !fenced_found && !indented_found {
448 None
450 } else if fenced_found && !indented_found {
451 Some(CodeBlockStyle::Fenced)
453 } else if !fenced_found && indented_found {
454 Some(CodeBlockStyle::Indented)
456 } else {
457 if indented_line < fenced_line {
459 Some(CodeBlockStyle::Indented)
460 } else {
461 Some(CodeBlockStyle::Fenced)
462 }
463 }
464 }
465}
466
467impl Rule for MD046CodeBlockStyle {
468 fn name(&self) -> &'static str {
469 "MD046"
470 }
471
472 fn description(&self) -> &'static str {
473 "Code blocks should use a consistent style"
474 }
475
476 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
477 if ctx.content.is_empty() {
479 return Ok(Vec::new());
480 }
481
482 if !ctx.content.contains("```") && !ctx.content.contains("~~~") && !ctx.content.contains(" ") {
484 return Ok(Vec::new());
485 }
486
487 let line_index = LineIndex::new(ctx.content.to_string());
489 let unclosed_warnings = self.check_unclosed_code_blocks(ctx, &line_index)?;
490
491 if !unclosed_warnings.is_empty() {
493 return Ok(unclosed_warnings);
494 }
495
496 let lines: Vec<&str> = ctx.content.lines().collect();
498 let mut warnings = Vec::new();
499
500 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
502
503 let target_style = match self.config.style {
505 CodeBlockStyle::Consistent => self
506 .detect_style(ctx.content, is_mkdocs)
507 .unwrap_or(CodeBlockStyle::Fenced),
508 _ => self.config.style,
509 };
510
511 let line_index = LineIndex::new(ctx.content.to_string());
513
514 let code_blocks = crate::utils::code_block_utils::CodeBlockUtils::detect_code_blocks(ctx.content);
517 let mut in_fenced_block = vec![false; lines.len()];
518 for &(start, end) in &code_blocks {
519 if start < ctx.content.len() && end <= ctx.content.len() {
521 let block_content = &ctx.content[start..end];
522 let is_fenced = block_content.starts_with("```") || block_content.starts_with("~~~");
523
524 if is_fenced {
525 for (line_idx, line_info) in ctx.lines.iter().enumerate() {
527 if line_info.byte_offset >= start && line_info.byte_offset < end {
528 in_fenced_block[line_idx] = true;
529 }
530 }
531 }
532 }
533 }
534
535 let mut in_fence = false;
536 for (i, line) in lines.iter().enumerate() {
537 let trimmed = line.trim_start();
538
539 if ctx.line_info(i + 1).is_some_and(|info| info.in_html_block) {
541 continue;
542 }
543
544 if ctx.lines[i].in_mkdocstrings {
547 continue;
548 }
549
550 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
552 if target_style == CodeBlockStyle::Indented && !in_fence {
553 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
556 warnings.push(LintWarning {
557 rule_name: Some(self.name().to_string()),
558 line: start_line,
559 column: start_col,
560 end_line,
561 end_column: end_col,
562 message: "Use indented code blocks".to_string(),
563 severity: Severity::Warning,
564 fix: Some(Fix {
565 range: line_index.line_col_to_byte_range(i + 1, 1),
566 replacement: String::new(),
567 }),
568 });
569 }
570 in_fence = !in_fence;
572 continue;
573 }
574
575 if in_fenced_block[i] {
578 continue;
579 }
580
581 if self.is_indented_code_block(&lines, i, is_mkdocs) && target_style == CodeBlockStyle::Fenced {
583 let prev_line_is_indented = i > 0 && self.is_indented_code_block(&lines, i - 1, is_mkdocs);
585
586 if !prev_line_is_indented {
587 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
588 warnings.push(LintWarning {
589 rule_name: Some(self.name().to_string()),
590 line: start_line,
591 column: start_col,
592 end_line,
593 end_column: end_col,
594 message: "Use fenced code blocks".to_string(),
595 severity: Severity::Warning,
596 fix: Some(Fix {
597 range: line_index.line_col_to_byte_range(i + 1, 1),
598 replacement: format!("```\n{}", line.trim_start()),
599 }),
600 });
601 }
602 }
603 }
604
605 Ok(warnings)
606 }
607
608 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
609 let content = ctx.content;
610 if content.is_empty() {
611 return Ok(String::new());
612 }
613
614 let line_index = LineIndex::new(ctx.content.to_string());
616 let unclosed_warnings = self.check_unclosed_code_blocks(ctx, &line_index)?;
617
618 if !unclosed_warnings.is_empty() {
620 for warning in &unclosed_warnings {
622 if warning
623 .message
624 .contains("should be closed before starting new one at line")
625 {
626 if let Some(fix) = &warning.fix {
628 let mut result = String::new();
629 result.push_str(&content[..fix.range.start]);
630 result.push_str(&fix.replacement);
631 result.push_str(&content[fix.range.start..]);
632 return Ok(result);
633 }
634 }
635 }
636 }
637
638 let lines: Vec<&str> = content.lines().collect();
639
640 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
642 let target_style = match self.config.style {
643 CodeBlockStyle::Consistent => self.detect_style(content, is_mkdocs).unwrap_or(CodeBlockStyle::Fenced),
644 _ => self.config.style,
645 };
646
647 let mut result = String::with_capacity(content.len());
648 let mut in_fenced_block = false;
649 let mut fenced_fence_type = None;
650 let mut in_indented_block = false;
651
652 for (i, line) in lines.iter().enumerate() {
653 let trimmed = line.trim_start();
654
655 if !in_fenced_block && (trimmed.starts_with("```") || trimmed.starts_with("~~~")) {
657 in_fenced_block = true;
658 fenced_fence_type = Some(if trimmed.starts_with("```") { "```" } else { "~~~" });
659
660 if target_style == CodeBlockStyle::Indented {
661 in_indented_block = true;
663 } else {
664 result.push_str(line);
666 result.push('\n');
667 }
668 } else if in_fenced_block && fenced_fence_type.is_some() {
669 let fence = fenced_fence_type.unwrap();
670 if trimmed.starts_with(fence) {
671 in_fenced_block = false;
672 fenced_fence_type = None;
673 in_indented_block = false;
674
675 if target_style == CodeBlockStyle::Indented {
676 } else {
678 result.push_str(line);
680 result.push('\n');
681 }
682 } else if target_style == CodeBlockStyle::Indented {
683 result.push_str(" ");
685 result.push_str(trimmed);
686 result.push('\n');
687 } else {
688 result.push_str(line);
690 result.push('\n');
691 }
692 } else if self.is_indented_code_block(&lines, i, is_mkdocs) {
693 let prev_line_is_indented = i > 0 && self.is_indented_code_block(&lines, i - 1, is_mkdocs);
697
698 if target_style == CodeBlockStyle::Fenced {
699 if !prev_line_is_indented && !in_indented_block {
700 result.push_str("```\n");
702 result.push_str(line.trim_start());
703 result.push('\n');
704 in_indented_block = true;
705 } else {
706 result.push_str(line.trim_start());
708 result.push('\n');
709 }
710
711 let _next_line_is_indented =
713 i < lines.len() - 1 && self.is_indented_code_block(&lines, i + 1, is_mkdocs);
714 if !_next_line_is_indented && in_indented_block {
715 result.push_str("```\n");
716 in_indented_block = false;
717 }
718 } else {
719 result.push_str(line);
721 result.push('\n');
722 }
723 } else {
724 if in_indented_block && target_style == CodeBlockStyle::Fenced {
726 result.push_str("```\n");
727 in_indented_block = false;
728 }
729
730 result.push_str(line);
731 result.push('\n');
732 }
733 }
734
735 if in_indented_block && target_style == CodeBlockStyle::Fenced {
737 result.push_str("```\n");
738 }
739
740 if let Some(fence_type) = fenced_fence_type
742 && in_fenced_block
743 {
744 result.push_str(fence_type);
745 result.push('\n');
746 }
747
748 if !content.ends_with('\n') && result.ends_with('\n') {
750 result.pop();
751 }
752
753 Ok(result)
754 }
755
756 fn category(&self) -> RuleCategory {
758 RuleCategory::CodeBlock
759 }
760
761 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
763 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
766 }
767
768 fn as_any(&self) -> &dyn std::any::Any {
769 self
770 }
771
772 fn default_config_section(&self) -> Option<(String, toml::Value)> {
773 let json_value = serde_json::to_value(&self.config).ok()?;
774 Some((
775 self.name().to_string(),
776 crate::rule_config_serde::json_to_toml_value(&json_value)?,
777 ))
778 }
779
780 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
781 where
782 Self: Sized,
783 {
784 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
785 Box::new(Self::from_config_struct(rule_config))
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use super::*;
792 use crate::lint_context::LintContext;
793
794 #[test]
795 fn test_fenced_code_block_detection() {
796 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
797 assert!(rule.is_fenced_code_block_start("```"));
798 assert!(rule.is_fenced_code_block_start("```rust"));
799 assert!(rule.is_fenced_code_block_start("~~~"));
800 assert!(rule.is_fenced_code_block_start("~~~python"));
801 assert!(rule.is_fenced_code_block_start(" ```"));
802 assert!(!rule.is_fenced_code_block_start("``"));
803 assert!(!rule.is_fenced_code_block_start("~~"));
804 assert!(!rule.is_fenced_code_block_start("Regular text"));
805 }
806
807 #[test]
808 fn test_consistent_style_with_fenced_blocks() {
809 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
810 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
811 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
812 let result = rule.check(&ctx).unwrap();
813
814 assert_eq!(result.len(), 0);
816 }
817
818 #[test]
819 fn test_consistent_style_with_indented_blocks() {
820 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
821 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
822 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
823 let result = rule.check(&ctx).unwrap();
824
825 assert_eq!(result.len(), 0);
827 }
828
829 #[test]
830 fn test_consistent_style_mixed() {
831 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
832 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
833 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
834 let result = rule.check(&ctx).unwrap();
835
836 assert!(!result.is_empty());
838 }
839
840 #[test]
841 fn test_fenced_style_with_indented_blocks() {
842 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
843 let content = "Text\n\n indented code\n more code\n\nMore text";
844 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
845 let result = rule.check(&ctx).unwrap();
846
847 assert!(!result.is_empty());
849 assert!(result[0].message.contains("Use fenced code blocks"));
850 }
851
852 #[test]
853 fn test_indented_style_with_fenced_blocks() {
854 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
855 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
856 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
857 let result = rule.check(&ctx).unwrap();
858
859 assert!(!result.is_empty());
861 assert!(result[0].message.contains("Use indented code blocks"));
862 }
863
864 #[test]
865 fn test_unclosed_code_block() {
866 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
867 let content = "```\ncode without closing fence";
868 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
869 let result = rule.check(&ctx).unwrap();
870
871 assert_eq!(result.len(), 1);
872 assert!(result[0].message.contains("never closed"));
873 }
874
875 #[test]
876 fn test_nested_code_blocks() {
877 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
878 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
879 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
880 let result = rule.check(&ctx).unwrap();
881
882 assert_eq!(result.len(), 0);
884 }
885
886 #[test]
887 fn test_fix_indented_to_fenced() {
888 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
889 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
890 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
891 let fixed = rule.fix(&ctx).unwrap();
892
893 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
894 }
895
896 #[test]
897 fn test_fix_fenced_to_indented() {
898 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
899 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
900 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
901 let fixed = rule.fix(&ctx).unwrap();
902
903 assert!(fixed.contains(" code line 1\n code line 2"));
904 assert!(!fixed.contains("```"));
905 }
906
907 #[test]
908 fn test_fix_unclosed_block() {
909 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
910 let content = "```\ncode without closing";
911 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
912 let fixed = rule.fix(&ctx).unwrap();
913
914 assert!(fixed.ends_with("```"));
916 }
917
918 #[test]
919 fn test_code_block_in_list() {
920 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
921 let content = "- List item\n code in list\n more code\n- Next item";
922 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
923 let result = rule.check(&ctx).unwrap();
924
925 assert_eq!(result.len(), 0);
927 }
928
929 #[test]
930 fn test_detect_style_fenced() {
931 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
932 let content = "```\ncode\n```";
933 let style = rule.detect_style(content, false);
934
935 assert_eq!(style, Some(CodeBlockStyle::Fenced));
936 }
937
938 #[test]
939 fn test_detect_style_indented() {
940 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
941 let content = "Text\n\n code\n\nMore";
942 let style = rule.detect_style(content, false);
943
944 assert_eq!(style, Some(CodeBlockStyle::Indented));
945 }
946
947 #[test]
948 fn test_detect_style_none() {
949 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
950 let content = "No code blocks here";
951 let style = rule.detect_style(content, false);
952
953 assert_eq!(style, None);
954 }
955
956 #[test]
957 fn test_tilde_fence() {
958 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
959 let content = "~~~\ncode\n~~~";
960 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
961 let result = rule.check(&ctx).unwrap();
962
963 assert_eq!(result.len(), 0);
965 }
966
967 #[test]
968 fn test_language_specification() {
969 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
970 let content = "```rust\nfn main() {}\n```";
971 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
972 let result = rule.check(&ctx).unwrap();
973
974 assert_eq!(result.len(), 0);
975 }
976
977 #[test]
978 fn test_empty_content() {
979 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
980 let content = "";
981 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
982 let result = rule.check(&ctx).unwrap();
983
984 assert_eq!(result.len(), 0);
985 }
986
987 #[test]
988 fn test_default_config() {
989 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
990 let (name, _config) = rule.default_config_section().unwrap();
991 assert_eq!(name, "MD046");
992 }
993
994 #[test]
995 fn test_markdown_documentation_block() {
996 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
997 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
998 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
999 let result = rule.check(&ctx).unwrap();
1000
1001 assert_eq!(result.len(), 0);
1003 }
1004
1005 #[test]
1006 fn test_preserve_trailing_newline() {
1007 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1008 let content = "```\ncode\n```\n";
1009 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1010 let fixed = rule.fix(&ctx).unwrap();
1011
1012 assert_eq!(fixed, content);
1013 }
1014
1015 #[test]
1016 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
1017 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1018 let content = r#"# Document
1019
1020=== "Python"
1021
1022 This is tab content
1023 Not an indented code block
1024
1025 ```python
1026 def hello():
1027 print("Hello")
1028 ```
1029
1030=== "JavaScript"
1031
1032 More tab content here
1033 Also not an indented code block"#;
1034
1035 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs);
1036 let result = rule.check(&ctx).unwrap();
1037
1038 assert_eq!(result.len(), 0);
1040 }
1041
1042 #[test]
1043 fn test_mkdocs_tabs_with_actual_indented_code() {
1044 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1045 let content = r#"# Document
1046
1047=== "Tab 1"
1048
1049 This is tab content
1050
1051Regular text
1052
1053 This is an actual indented code block
1054 Should be flagged"#;
1055
1056 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs);
1057 let result = rule.check(&ctx).unwrap();
1058
1059 assert_eq!(result.len(), 1);
1061 assert!(result[0].message.contains("Use fenced code blocks"));
1062 }
1063
1064 #[test]
1065 fn test_mkdocs_tabs_detect_style() {
1066 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1067 let content = r#"=== "Tab 1"
1068
1069 Content in tab
1070 More content
1071
1072=== "Tab 2"
1073
1074 Content in second tab"#;
1075
1076 let style = rule.detect_style(content, true);
1078 assert_eq!(style, None); let style = rule.detect_style(content, false);
1082 assert_eq!(style, Some(CodeBlockStyle::Indented));
1083 }
1084
1085 #[test]
1086 fn test_mkdocs_nested_tabs() {
1087 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1088 let content = r#"# Document
1089
1090=== "Outer Tab"
1091
1092 Some content
1093
1094 === "Nested Tab"
1095
1096 Nested tab content
1097 Should not be flagged"#;
1098
1099 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs);
1100 let result = rule.check(&ctx).unwrap();
1101
1102 assert_eq!(result.len(), 0);
1104 }
1105}