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