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()),
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()),
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 mut in_fenced_block = false;
513 let line_index = LineIndex::new(ctx.content.to_string());
514
515 for (i, line) in lines.iter().enumerate() {
516 let trimmed = line.trim_start();
517
518 if ctx.is_in_html_block(i + 1) {
520 continue;
521 }
522
523 if ctx.lines[i].in_mkdocstrings {
526 continue;
527 }
528
529 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
531 in_fenced_block = !in_fenced_block;
532
533 if target_style == CodeBlockStyle::Indented && !in_fenced_block {
534 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
536 warnings.push(LintWarning {
537 rule_name: Some(self.name()),
538 line: start_line,
539 column: start_col,
540 end_line,
541 end_column: end_col,
542 message: "Use indented code blocks".to_string(),
543 severity: Severity::Warning,
544 fix: Some(Fix {
545 range: line_index.line_col_to_byte_range(i + 1, 1),
546 replacement: String::new(),
547 }),
548 });
549 }
550 }
551 else if !in_fenced_block
553 && self.is_indented_code_block(&lines, i, is_mkdocs)
554 && target_style == CodeBlockStyle::Fenced
555 {
556 let prev_line_is_indented = i > 0 && self.is_indented_code_block(&lines, i - 1, is_mkdocs);
558
559 if !prev_line_is_indented {
560 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
561 warnings.push(LintWarning {
562 rule_name: Some(self.name()),
563 line: start_line,
564 column: start_col,
565 end_line,
566 end_column: end_col,
567 message: "Use fenced code blocks".to_string(),
568 severity: Severity::Warning,
569 fix: Some(Fix {
570 range: line_index.line_col_to_byte_range(i + 1, 1),
571 replacement: format!("```\n{}", line.trim_start()),
572 }),
573 });
574 }
575 }
576 }
577
578 Ok(warnings)
579 }
580
581 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
582 let content = ctx.content;
583 if content.is_empty() {
584 return Ok(String::new());
585 }
586
587 let line_index = LineIndex::new(ctx.content.to_string());
589 let unclosed_warnings = self.check_unclosed_code_blocks(ctx, &line_index)?;
590
591 if !unclosed_warnings.is_empty() {
593 for warning in &unclosed_warnings {
595 if warning
596 .message
597 .contains("should be closed before starting new one at line")
598 {
599 if let Some(fix) = &warning.fix {
601 let mut result = String::new();
602 result.push_str(&content[..fix.range.start]);
603 result.push_str(&fix.replacement);
604 result.push_str(&content[fix.range.start..]);
605 return Ok(result);
606 }
607 }
608 }
609 }
610
611 let lines: Vec<&str> = content.lines().collect();
612
613 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
615 let target_style = match self.config.style {
616 CodeBlockStyle::Consistent => self.detect_style(content, is_mkdocs).unwrap_or(CodeBlockStyle::Fenced),
617 _ => self.config.style,
618 };
619
620 let mut result = String::with_capacity(content.len());
621 let mut in_fenced_block = false;
622 let mut fenced_fence_type = None;
623 let mut in_indented_block = false;
624
625 for (i, line) in lines.iter().enumerate() {
626 let trimmed = line.trim_start();
627
628 if !in_fenced_block && (trimmed.starts_with("```") || trimmed.starts_with("~~~")) {
630 in_fenced_block = true;
631 fenced_fence_type = Some(if trimmed.starts_with("```") { "```" } else { "~~~" });
632
633 if target_style == CodeBlockStyle::Indented {
634 in_indented_block = true;
636 } else {
637 result.push_str(line);
639 result.push('\n');
640 }
641 } else if in_fenced_block && fenced_fence_type.is_some() {
642 let fence = fenced_fence_type.unwrap();
643 if trimmed.starts_with(fence) {
644 in_fenced_block = false;
645 fenced_fence_type = None;
646 in_indented_block = false;
647
648 if target_style == CodeBlockStyle::Indented {
649 } else {
651 result.push_str(line);
653 result.push('\n');
654 }
655 } else if target_style == CodeBlockStyle::Indented {
656 result.push_str(" ");
658 result.push_str(trimmed);
659 result.push('\n');
660 } else {
661 result.push_str(line);
663 result.push('\n');
664 }
665 } else if self.is_indented_code_block(&lines, i, is_mkdocs) {
666 let prev_line_is_indented = i > 0 && self.is_indented_code_block(&lines, i - 1, is_mkdocs);
670
671 if target_style == CodeBlockStyle::Fenced {
672 if !prev_line_is_indented && !in_indented_block {
673 result.push_str("```\n");
675 result.push_str(line.trim_start());
676 result.push('\n');
677 in_indented_block = true;
678 } else {
679 result.push_str(line.trim_start());
681 result.push('\n');
682 }
683
684 let _next_line_is_indented =
686 i < lines.len() - 1 && self.is_indented_code_block(&lines, i + 1, is_mkdocs);
687 if !_next_line_is_indented && in_indented_block {
688 result.push_str("```\n");
689 in_indented_block = false;
690 }
691 } else {
692 result.push_str(line);
694 result.push('\n');
695 }
696 } else {
697 if in_indented_block && target_style == CodeBlockStyle::Fenced {
699 result.push_str("```\n");
700 in_indented_block = false;
701 }
702
703 result.push_str(line);
704 result.push('\n');
705 }
706 }
707
708 if in_indented_block && target_style == CodeBlockStyle::Fenced {
710 result.push_str("```\n");
711 }
712
713 if let Some(fence_type) = fenced_fence_type
715 && in_fenced_block
716 {
717 result.push_str(fence_type);
718 result.push('\n');
719 }
720
721 if !content.ends_with('\n') && result.ends_with('\n') {
723 result.pop();
724 }
725
726 Ok(result)
727 }
728
729 fn category(&self) -> RuleCategory {
731 RuleCategory::CodeBlock
732 }
733
734 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
736 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
739 }
740
741 fn as_any(&self) -> &dyn std::any::Any {
742 self
743 }
744
745 fn default_config_section(&self) -> Option<(String, toml::Value)> {
746 let json_value = serde_json::to_value(&self.config).ok()?;
747 Some((
748 self.name().to_string(),
749 crate::rule_config_serde::json_to_toml_value(&json_value)?,
750 ))
751 }
752
753 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
754 where
755 Self: Sized,
756 {
757 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
758 Box::new(Self::from_config_struct(rule_config))
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765 use crate::lint_context::LintContext;
766
767 #[test]
768 fn test_fenced_code_block_detection() {
769 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
770 assert!(rule.is_fenced_code_block_start("```"));
771 assert!(rule.is_fenced_code_block_start("```rust"));
772 assert!(rule.is_fenced_code_block_start("~~~"));
773 assert!(rule.is_fenced_code_block_start("~~~python"));
774 assert!(rule.is_fenced_code_block_start(" ```"));
775 assert!(!rule.is_fenced_code_block_start("``"));
776 assert!(!rule.is_fenced_code_block_start("~~"));
777 assert!(!rule.is_fenced_code_block_start("Regular text"));
778 }
779
780 #[test]
781 fn test_consistent_style_with_fenced_blocks() {
782 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
783 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
784 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
785 let result = rule.check(&ctx).unwrap();
786
787 assert_eq!(result.len(), 0);
789 }
790
791 #[test]
792 fn test_consistent_style_with_indented_blocks() {
793 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
794 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
795 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
796 let result = rule.check(&ctx).unwrap();
797
798 assert_eq!(result.len(), 0);
800 }
801
802 #[test]
803 fn test_consistent_style_mixed() {
804 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
805 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
806 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
807 let result = rule.check(&ctx).unwrap();
808
809 assert!(!result.is_empty());
811 }
812
813 #[test]
814 fn test_fenced_style_with_indented_blocks() {
815 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
816 let content = "Text\n\n indented code\n more code\n\nMore text";
817 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
818 let result = rule.check(&ctx).unwrap();
819
820 assert!(!result.is_empty());
822 assert!(result[0].message.contains("Use fenced code blocks"));
823 }
824
825 #[test]
826 fn test_indented_style_with_fenced_blocks() {
827 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
828 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
829 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
830 let result = rule.check(&ctx).unwrap();
831
832 assert!(!result.is_empty());
834 assert!(result[0].message.contains("Use indented code blocks"));
835 }
836
837 #[test]
838 fn test_unclosed_code_block() {
839 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
840 let content = "```\ncode without closing fence";
841 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
842 let result = rule.check(&ctx).unwrap();
843
844 assert_eq!(result.len(), 1);
845 assert!(result[0].message.contains("never closed"));
846 }
847
848 #[test]
849 fn test_nested_code_blocks() {
850 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
851 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
852 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
853 let result = rule.check(&ctx).unwrap();
854
855 assert_eq!(result.len(), 0);
857 }
858
859 #[test]
860 fn test_fix_indented_to_fenced() {
861 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
862 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
863 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
864 let fixed = rule.fix(&ctx).unwrap();
865
866 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
867 }
868
869 #[test]
870 fn test_fix_fenced_to_indented() {
871 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
872 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
873 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
874 let fixed = rule.fix(&ctx).unwrap();
875
876 assert!(fixed.contains(" code line 1\n code line 2"));
877 assert!(!fixed.contains("```"));
878 }
879
880 #[test]
881 fn test_fix_unclosed_block() {
882 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
883 let content = "```\ncode without closing";
884 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
885 let fixed = rule.fix(&ctx).unwrap();
886
887 assert!(fixed.ends_with("```"));
889 }
890
891 #[test]
892 fn test_code_block_in_list() {
893 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
894 let content = "- List item\n code in list\n more code\n- Next item";
895 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
896 let result = rule.check(&ctx).unwrap();
897
898 assert_eq!(result.len(), 0);
900 }
901
902 #[test]
903 fn test_detect_style_fenced() {
904 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
905 let content = "```\ncode\n```";
906 let style = rule.detect_style(content, false);
907
908 assert_eq!(style, Some(CodeBlockStyle::Fenced));
909 }
910
911 #[test]
912 fn test_detect_style_indented() {
913 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
914 let content = "Text\n\n code\n\nMore";
915 let style = rule.detect_style(content, false);
916
917 assert_eq!(style, Some(CodeBlockStyle::Indented));
918 }
919
920 #[test]
921 fn test_detect_style_none() {
922 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
923 let content = "No code blocks here";
924 let style = rule.detect_style(content, false);
925
926 assert_eq!(style, None);
927 }
928
929 #[test]
930 fn test_tilde_fence() {
931 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
932 let content = "~~~\ncode\n~~~";
933 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
934 let result = rule.check(&ctx).unwrap();
935
936 assert_eq!(result.len(), 0);
938 }
939
940 #[test]
941 fn test_language_specification() {
942 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
943 let content = "```rust\nfn main() {}\n```";
944 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
945 let result = rule.check(&ctx).unwrap();
946
947 assert_eq!(result.len(), 0);
948 }
949
950 #[test]
951 fn test_empty_content() {
952 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
953 let content = "";
954 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
955 let result = rule.check(&ctx).unwrap();
956
957 assert_eq!(result.len(), 0);
958 }
959
960 #[test]
961 fn test_default_config() {
962 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
963 let (name, _config) = rule.default_config_section().unwrap();
964 assert_eq!(name, "MD046");
965 }
966
967 #[test]
968 fn test_markdown_documentation_block() {
969 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
970 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\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);
976 }
977
978 #[test]
979 fn test_preserve_trailing_newline() {
980 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
981 let content = "```\ncode\n```\n";
982 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
983 let fixed = rule.fix(&ctx).unwrap();
984
985 assert_eq!(fixed, content);
986 }
987
988 #[test]
989 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
990 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
991 let content = r#"# Document
992
993=== "Python"
994
995 This is tab content
996 Not an indented code block
997
998 ```python
999 def hello():
1000 print("Hello")
1001 ```
1002
1003=== "JavaScript"
1004
1005 More tab content here
1006 Also not an indented code block"#;
1007
1008 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs);
1009 let result = rule.check(&ctx).unwrap();
1010
1011 assert_eq!(result.len(), 0);
1013 }
1014
1015 #[test]
1016 fn test_mkdocs_tabs_with_actual_indented_code() {
1017 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1018 let content = r#"# Document
1019
1020=== "Tab 1"
1021
1022 This is tab content
1023
1024Regular text
1025
1026 This is an actual indented code block
1027 Should be flagged"#;
1028
1029 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs);
1030 let result = rule.check(&ctx).unwrap();
1031
1032 assert_eq!(result.len(), 1);
1034 assert!(result[0].message.contains("Use fenced code blocks"));
1035 }
1036
1037 #[test]
1038 fn test_mkdocs_tabs_detect_style() {
1039 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1040 let content = r#"=== "Tab 1"
1041
1042 Content in tab
1043 More content
1044
1045=== "Tab 2"
1046
1047 Content in second tab"#;
1048
1049 let style = rule.detect_style(content, true);
1051 assert_eq!(style, None); let style = rule.detect_style(content, false);
1055 assert_eq!(style, Some(CodeBlockStyle::Indented));
1056 }
1057
1058 #[test]
1059 fn test_mkdocs_nested_tabs() {
1060 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1061 let content = r#"# Document
1062
1063=== "Outer Tab"
1064
1065 Some content
1066
1067 === "Nested Tab"
1068
1069 Nested tab content
1070 Should not be flagged"#;
1071
1072 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs);
1073 let result = rule.check(&ctx).unwrap();
1074
1075 assert_eq!(result.len(), 0);
1077 }
1078}