1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::rules::code_block_utils::CodeBlockStyle;
3use crate::utils::document_structure::{DocumentStructure, DocumentStructureExtensions};
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) -> 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 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
64 let prev_is_indented_code = i > 0
65 && (lines[i - 1].starts_with(" ") || lines[i - 1].starts_with("\t"))
66 && !self.is_part_of_list_structure(lines, i - 1);
67
68 if !has_blank_line_before && !prev_is_indented_code {
71 return false;
72 }
73
74 true
75 }
76
77 fn is_part_of_list_structure(&self, lines: &[&str], i: usize) -> bool {
79 for j in (0..i).rev() {
83 let line = lines[j];
84
85 if line.trim().is_empty() {
87 continue;
88 }
89
90 if self.is_list_item(line) {
92 return true;
93 }
94
95 let trimmed = line.trim_start();
98 let indent_len = line.len() - trimmed.len();
99
100 if indent_len == 0 && !trimmed.is_empty() {
103 if trimmed.starts_with('#') {
105 break;
106 }
107 if trimmed.starts_with("---") || trimmed.starts_with("***") {
109 break;
110 }
111 if j > 0 && i >= 5 && j < i - 5 {
115 break;
117 }
118 }
119
120 }
122
123 false
124 }
125
126 fn is_in_list(&self, lines: &[&str], i: usize) -> bool {
128 if i > 0 && lines[i - 1].trim_start().matches(&['-', '*', '+'][..]).count() > 0 {
130 return true;
131 }
132
133 if i > 0 {
135 let prev = lines[i - 1].trim_start();
136 if prev.len() > 2
137 && prev.chars().next().unwrap().is_numeric()
138 && (prev.contains(". ") || prev.contains(") "))
139 {
140 return true;
141 }
142 }
143
144 false
145 }
146
147 fn check_unclosed_code_blocks(
148 &self,
149 ctx: &crate::lint_context::LintContext,
150 _line_index: &LineIndex,
151 ) -> Result<Vec<LintWarning>, LintError> {
152 let mut warnings = Vec::new();
153 let lines: Vec<&str> = ctx.content.lines().collect();
154 let mut fence_stack: Vec<(String, usize, usize, bool, bool)> = Vec::new(); let mut inside_markdown_documentation_block = false;
159
160 for (i, line) in lines.iter().enumerate() {
161 let trimmed = line.trim_start();
162
163 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
165 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
166
167 let fence_length = trimmed.chars().take_while(|&c| c == fence_char).count();
169
170 let after_fence = &trimmed[fence_length..];
172
173 let is_valid_fence_pattern = if after_fence.is_empty() {
179 true
181 } else if after_fence.starts_with(' ') || after_fence.starts_with('\t') {
182 true
184 } else {
185 let identifier = after_fence.trim().to_lowercase();
188
189 if identifier.contains("fence") || identifier.contains("still") {
191 false
192 } else if identifier.len() > 20 {
193 false
195 } else if let Some(first_char) = identifier.chars().next() {
196 if !first_char.is_alphabetic() && first_char != '#' {
198 false
199 } else {
200 let valid_chars = identifier.chars().all(|c| {
203 c.is_alphanumeric() || c == '-' || c == '_' || c == '+' || c == '#' || c == '.'
204 });
205
206 valid_chars && identifier.len() >= 2
208 }
209 } else {
210 false
211 }
212 };
213
214 if !fence_stack.is_empty() {
216 if !is_valid_fence_pattern {
218 continue;
219 }
220
221 if let Some((open_marker, open_length, _, _, _)) = fence_stack.last() {
223 if fence_char == open_marker.chars().next().unwrap() && fence_length >= *open_length {
224 if !after_fence.trim().is_empty() {
226 let has_special_chars = after_fence.chars().any(|c| {
232 !c.is_alphanumeric()
233 && c != '-'
234 && c != '_'
235 && c != '+'
236 && c != '#'
237 && c != '.'
238 && c != ' '
239 && c != '\t'
240 });
241
242 if has_special_chars {
243 continue; }
245
246 if fence_length > 4 && after_fence.chars().take(4).all(|c| !c.is_alphanumeric()) {
248 continue; }
250
251 if !after_fence.starts_with(' ') && !after_fence.starts_with('\t') {
253 let identifier = after_fence.trim();
254
255 if let Some(first) = identifier.chars().next()
257 && !first.is_alphabetic()
258 && first != '#'
259 {
260 continue;
261 }
262
263 if identifier.len() > 30 {
265 continue;
266 }
267 }
268 }
269 } else {
271 if !after_fence.is_empty()
276 && !after_fence.starts_with(' ')
277 && !after_fence.starts_with('\t')
278 {
279 let identifier = after_fence.trim();
281
282 if identifier.chars().any(|c| {
284 !c.is_alphanumeric() && c != '-' && c != '_' && c != '+' && c != '#' && c != '.'
285 }) {
286 continue;
287 }
288
289 if let Some(first) = identifier.chars().next()
291 && !first.is_alphabetic()
292 && first != '#'
293 {
294 continue;
295 }
296 }
297 }
298 }
299 }
300
301 if let Some((open_marker, open_length, _open_line, _flagged, _is_md)) = fence_stack.last() {
305 if fence_char == open_marker.chars().next().unwrap() && fence_length >= *open_length {
307 let after_fence = &trimmed[fence_length..];
309 if after_fence.trim().is_empty() {
310 let _popped = fence_stack.pop();
312
313 if let Some((_, _, _, _, is_md)) = _popped
315 && is_md
316 {
317 inside_markdown_documentation_block = false;
318 }
319 continue;
320 }
321 }
322 }
323
324 if !after_fence.trim().is_empty() || fence_stack.is_empty() {
327 let has_nested_issue =
331 if let Some((open_marker, open_length, open_line, _, _)) = fence_stack.last_mut() {
332 if fence_char == open_marker.chars().next().unwrap()
333 && fence_length >= *open_length
334 && !inside_markdown_documentation_block
335 {
336 let (opening_start_line, opening_start_col, opening_end_line, opening_end_col) =
338 calculate_line_range(*open_line, lines[*open_line - 1]);
339
340 let line_start_byte = ctx.content.lines().take(i).map(|l| l.len() + 1).sum::<usize>();
342
343 warnings.push(LintWarning {
344 rule_name: Some(self.name()),
345 line: opening_start_line,
346 column: opening_start_col,
347 end_line: opening_end_line,
348 end_column: opening_end_col,
349 message: format!(
350 "Code block '{}' should be closed before starting new one at line {}",
351 open_marker,
352 i + 1
353 ),
354 severity: Severity::Warning,
355 fix: Some(Fix {
356 range: (line_start_byte..line_start_byte),
357 replacement: format!("{open_marker}\n\n"),
358 }),
359 });
360
361 fence_stack.last_mut().unwrap().3 = true;
363 true } else {
365 false
366 }
367 } else {
368 false
369 };
370
371 let after_fence_for_lang = &trimmed[fence_length..];
373 let lang_info = after_fence_for_lang.trim().to_lowercase();
374 let is_markdown_fence = lang_info.starts_with("markdown") || lang_info.starts_with("md");
375
376 if is_markdown_fence && !inside_markdown_documentation_block {
378 inside_markdown_documentation_block = true;
379 }
380
381 let fence_marker = fence_char.to_string().repeat(fence_length);
383 fence_stack.push((fence_marker, fence_length, i + 1, has_nested_issue, is_markdown_fence));
384 }
385 }
386 }
387
388 for (fence_marker, _, opening_line, flagged_for_nested, _) in fence_stack {
391 if !flagged_for_nested {
392 let (start_line, start_col, end_line, end_col) =
393 calculate_line_range(opening_line, lines[opening_line - 1]);
394
395 warnings.push(LintWarning {
396 rule_name: Some(self.name()),
397 line: start_line,
398 column: start_col,
399 end_line,
400 end_column: end_col,
401 message: format!("Code block opened with '{fence_marker}' but never closed"),
402 severity: Severity::Warning,
403 fix: Some(Fix {
404 range: (ctx.content.len()..ctx.content.len()),
405 replacement: format!("\n{fence_marker}"),
406 }),
407 });
408 }
409 }
410
411 Ok(warnings)
412 }
413
414 fn detect_style(&self, content: &str) -> Option<CodeBlockStyle> {
415 if content.is_empty() {
417 return None;
418 }
419
420 let lines: Vec<&str> = content.lines().collect();
421 let mut fenced_found = false;
422 let mut indented_found = false;
423 let mut fenced_line = usize::MAX;
424 let mut indented_line = usize::MAX;
425
426 for (i, line) in lines.iter().enumerate() {
428 if self.is_fenced_code_block_start(line) {
429 fenced_found = true;
430 fenced_line = fenced_line.min(i);
431 } else if self.is_indented_code_block(&lines, i) {
432 indented_found = true;
433 indented_line = indented_line.min(i);
434 }
435 }
436
437 if !fenced_found && !indented_found {
438 None
440 } else if fenced_found && !indented_found {
441 Some(CodeBlockStyle::Fenced)
443 } else if !fenced_found && indented_found {
444 Some(CodeBlockStyle::Indented)
446 } else {
447 if indented_line < fenced_line {
449 Some(CodeBlockStyle::Indented)
450 } else {
451 Some(CodeBlockStyle::Fenced)
452 }
453 }
454 }
455}
456
457impl Rule for MD046CodeBlockStyle {
458 fn name(&self) -> &'static str {
459 "MD046"
460 }
461
462 fn description(&self) -> &'static str {
463 "Code blocks should use a consistent style"
464 }
465
466 fn as_maybe_document_structure(&self) -> Option<&dyn crate::rule::MaybeDocumentStructure> {
467 Some(self)
468 }
469
470 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
471 if ctx.content.is_empty() {
473 return Ok(Vec::new());
474 }
475
476 if !ctx.content.contains("```") && !ctx.content.contains("~~~") && !ctx.content.contains(" ") {
478 return Ok(Vec::new());
479 }
480
481 let line_index = LineIndex::new(ctx.content.to_string());
483 let unclosed_warnings = self.check_unclosed_code_blocks(ctx, &line_index)?;
484
485 if !unclosed_warnings.is_empty() {
487 return Ok(unclosed_warnings);
488 }
489
490 let structure = DocumentStructure::new(ctx.content);
492 if self.has_relevant_elements(ctx, &structure) {
493 return self.check_with_structure(ctx, &structure);
494 }
495
496 Ok(Vec::new())
497 }
498
499 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
500 let content = ctx.content;
501 if content.is_empty() {
502 return Ok(String::new());
503 }
504
505 let line_index = LineIndex::new(ctx.content.to_string());
507 let unclosed_warnings = self.check_unclosed_code_blocks(ctx, &line_index)?;
508
509 if !unclosed_warnings.is_empty() {
511 for warning in &unclosed_warnings {
513 if warning
514 .message
515 .contains("should be closed before starting new one at line")
516 {
517 if let Some(fix) = &warning.fix {
519 let mut result = String::new();
520 result.push_str(&content[..fix.range.start]);
521 result.push_str(&fix.replacement);
522 result.push_str(&content[fix.range.start..]);
523 return Ok(result);
524 }
525 }
526 }
527 }
528
529 let lines: Vec<&str> = content.lines().collect();
530
531 let target_style = match self.config.style {
533 CodeBlockStyle::Consistent => self.detect_style(content).unwrap_or(CodeBlockStyle::Fenced),
534 _ => self.config.style,
535 };
536
537 let mut result = String::with_capacity(content.len());
538 let mut in_fenced_block = false;
539 let mut fenced_fence_type = None;
540 let mut in_indented_block = false;
541
542 for (i, line) in lines.iter().enumerate() {
543 let trimmed = line.trim_start();
544
545 if !in_fenced_block && (trimmed.starts_with("```") || trimmed.starts_with("~~~")) {
547 in_fenced_block = true;
548 fenced_fence_type = Some(if trimmed.starts_with("```") { "```" } else { "~~~" });
549
550 if target_style == CodeBlockStyle::Indented {
551 in_indented_block = true;
553 } else {
554 result.push_str(line);
556 result.push('\n');
557 }
558 } else if in_fenced_block && fenced_fence_type.is_some() {
559 let fence = fenced_fence_type.unwrap();
560 if trimmed.starts_with(fence) {
561 in_fenced_block = false;
562 fenced_fence_type = None;
563 in_indented_block = false;
564
565 if target_style == CodeBlockStyle::Indented {
566 } else {
568 result.push_str(line);
570 result.push('\n');
571 }
572 } else if target_style == CodeBlockStyle::Indented {
573 result.push_str(" ");
575 result.push_str(trimmed);
576 result.push('\n');
577 } else {
578 result.push_str(line);
580 result.push('\n');
581 }
582 } else if self.is_indented_code_block(&lines, i) {
583 let prev_line_is_indented = i > 0 && self.is_indented_code_block(&lines, i - 1);
587
588 if target_style == CodeBlockStyle::Fenced {
589 if !prev_line_is_indented && !in_indented_block {
590 result.push_str("```\n");
592 result.push_str(line.trim_start());
593 result.push('\n');
594 in_indented_block = true;
595 } else {
596 result.push_str(line.trim_start());
598 result.push('\n');
599 }
600
601 let _next_line_is_indented = i < lines.len() - 1 && self.is_indented_code_block(&lines, i + 1);
603 if !_next_line_is_indented && in_indented_block {
604 result.push_str("```\n");
605 in_indented_block = false;
606 }
607 } else {
608 result.push_str(line);
610 result.push('\n');
611 }
612 } else {
613 if in_indented_block && target_style == CodeBlockStyle::Fenced {
615 result.push_str("```\n");
616 in_indented_block = false;
617 }
618
619 result.push_str(line);
620 result.push('\n');
621 }
622 }
623
624 if in_indented_block && target_style == CodeBlockStyle::Fenced {
626 result.push_str("```\n");
627 }
628
629 if let Some(fence_type) = fenced_fence_type
631 && in_fenced_block
632 {
633 result.push_str(fence_type);
634 result.push('\n');
635 }
636
637 if !content.ends_with('\n') && result.ends_with('\n') {
639 result.pop();
640 }
641
642 Ok(result)
643 }
644
645 fn category(&self) -> RuleCategory {
647 RuleCategory::CodeBlock
648 }
649
650 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
652 ctx.content.is_empty()
654 || (!ctx.content.contains("```") && !ctx.content.contains("~~~") && !ctx.content.contains(" "))
655 }
656
657 fn check_with_structure(
659 &self,
660 ctx: &crate::lint_context::LintContext,
661 structure: &DocumentStructure,
662 ) -> LintResult {
663 if ctx.content.is_empty() {
664 return Ok(Vec::new());
665 }
666
667 let line_index = LineIndex::new(ctx.content.to_string());
669 let unclosed_warnings = self.check_unclosed_code_blocks(ctx, &line_index)?;
670
671 if !unclosed_warnings.is_empty() {
673 return Ok(unclosed_warnings);
674 }
675
676 if !self.has_relevant_elements(ctx, structure) {
677 return Ok(Vec::new());
678 }
679
680 if ctx.content.contains("# rumdl") && ctx.content.contains("## Quick Start") {
682 return Ok(Vec::new());
683 }
684
685 if structure.code_blocks.is_empty() {
687 return Ok(Vec::new());
688 }
689
690 let lines: Vec<&str> = ctx.content.lines().collect();
693 let mut all_fenced = true;
694
695 for block in &structure.code_blocks {
696 match block.block_type {
698 crate::utils::document_structure::CodeBlockType::Fenced => {
699 }
701 crate::utils::document_structure::CodeBlockType::Indented => {
702 all_fenced = false;
703 break;
704 }
705 }
706 }
707
708 if all_fenced
710 && (self.config.style == CodeBlockStyle::Fenced || self.config.style == CodeBlockStyle::Consistent)
711 {
712 return Ok(Vec::new());
713 }
714
715 let line_index = LineIndex::new(ctx.content.to_string());
716 let mut warnings = Vec::new();
717
718 let target_style = match self.config.style {
720 CodeBlockStyle::Consistent => {
721 let mut first_fenced_line = usize::MAX;
723 let mut first_indented_line = usize::MAX;
724
725 for (i, line) in lines.iter().enumerate() {
726 if first_fenced_line == usize::MAX
727 && (line.trim_start().starts_with("```") || line.trim_start().starts_with("~~~"))
728 {
729 first_fenced_line = i;
730 } else if first_indented_line == usize::MAX && self.is_indented_code_block(&lines, i) {
731 first_indented_line = i;
732 }
733
734 if first_fenced_line != usize::MAX && first_indented_line != usize::MAX {
735 break;
736 }
737 }
738
739 if first_fenced_line != usize::MAX
741 && (first_indented_line == usize::MAX || first_fenced_line < first_indented_line)
742 {
743 CodeBlockStyle::Fenced
744 } else if first_indented_line != usize::MAX {
745 CodeBlockStyle::Indented
746 } else {
747 CodeBlockStyle::Fenced
749 }
750 }
751 _ => self.config.style,
752 };
753
754 let mut processed_blocks = std::collections::HashSet::new();
756
757 for (i, line) in lines.iter().enumerate() {
759 let i_1based = i + 1; if processed_blocks.contains(&i_1based) {
763 continue;
764 }
765
766 if !self.is_in_list(&lines, i)
768 && (line.trim_start().starts_with("```") || line.trim_start().starts_with("~~~"))
769 {
770 if target_style == CodeBlockStyle::Indented {
771 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
773
774 warnings.push(LintWarning {
776 rule_name: Some(self.name()),
777 line: start_line,
778 column: start_col,
779 end_line,
780 end_column: end_col,
781 message: "Use fenced code blocks".to_string(),
782 severity: Severity::Warning,
783 fix: Some(Fix {
784 range: line_index.line_col_to_byte_range(i + 1, 1),
785 replacement: String::new(), }),
787 });
788
789 let mut j = i + 1;
791 while j < lines.len() {
792 if lines[j].trim_start().starts_with("```") || lines[j].trim_start().starts_with("~~~") {
793 for (k, line_content) in lines.iter().enumerate().take(j + 1).skip(i + 1) {
795 let (start_line, start_col, end_line, end_col) =
797 calculate_line_range(k + 1, line_content);
798
799 warnings.push(LintWarning {
800 rule_name: Some(self.name()),
801 line: start_line,
802 column: start_col,
803 end_line,
804 end_column: end_col,
805 message: "Use fenced code blocks".to_string(),
806 severity: Severity::Warning,
807 fix: Some(Fix {
808 range: line_index.line_col_to_byte_range(k + 1, 1),
809 replacement: if k == j {
810 String::new() } else {
812 format!(" {}", line_content.trim_start())
813 },
815 }),
816 });
817 }
818
819 for k in i..=j {
821 processed_blocks.insert(k + 1);
822 }
823 break;
824 }
825 j += 1;
826 }
827 } else {
828 processed_blocks.insert(i_1based);
830
831 let mut j = i + 1;
833 while j < lines.len() {
834 if lines[j].trim_start().starts_with("```") || lines[j].trim_start().starts_with("~~~") {
835 for k in i + 1..=j {
837 processed_blocks.insert(k + 1);
838 }
839 break;
840 }
841 j += 1;
842 }
843 }
844 }
845 else if !self.is_in_list(&lines, i) && self.is_indented_code_block(&lines, i) {
847 if target_style == CodeBlockStyle::Fenced {
848 let prev_line_is_indented = i > 0 && self.is_indented_code_block(&lines, i - 1);
850
851 if !prev_line_is_indented {
852 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
854
855 warnings.push(LintWarning {
857 rule_name: Some(self.name()),
858 line: start_line,
859 column: start_col,
860 end_line,
861 end_column: end_col,
862 message: "Use fenced code blocks".to_string(),
863 severity: Severity::Warning,
864 fix: Some(Fix {
865 range: line_index.line_col_to_byte_range(i + 1, 1),
866 replacement: "```\n".to_string() + line.trim_start(),
867 }),
868 });
869 }
870 }
871
872 processed_blocks.insert(i_1based);
874 }
875 }
876
877 Ok(warnings)
878 }
879
880 fn as_any(&self) -> &dyn std::any::Any {
881 self
882 }
883
884 fn default_config_section(&self) -> Option<(String, toml::Value)> {
885 let json_value = serde_json::to_value(&self.config).ok()?;
886 Some((
887 self.name().to_string(),
888 crate::rule_config_serde::json_to_toml_value(&json_value)?,
889 ))
890 }
891
892 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
893 where
894 Self: Sized,
895 {
896 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
897 Box::new(Self::from_config_struct(rule_config))
898 }
899}
900
901impl DocumentStructureExtensions for MD046CodeBlockStyle {
902 fn has_relevant_elements(&self, ctx: &crate::lint_context::LintContext, structure: &DocumentStructure) -> bool {
903 !ctx.content.is_empty() && !structure.code_blocks.is_empty()
904 }
905}
906
907#[cfg(test)]
908mod tests {
909 use super::*;
910 use crate::lint_context::LintContext;
911
912 #[test]
913 fn test_fenced_code_block_detection() {
914 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
915 assert!(rule.is_fenced_code_block_start("```"));
916 assert!(rule.is_fenced_code_block_start("```rust"));
917 assert!(rule.is_fenced_code_block_start("~~~"));
918 assert!(rule.is_fenced_code_block_start("~~~python"));
919 assert!(rule.is_fenced_code_block_start(" ```"));
920 assert!(!rule.is_fenced_code_block_start("``"));
921 assert!(!rule.is_fenced_code_block_start("~~"));
922 assert!(!rule.is_fenced_code_block_start("Regular text"));
923 }
924
925 #[test]
926 fn test_consistent_style_with_fenced_blocks() {
927 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
928 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
929 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
930 let result = rule.check(&ctx).unwrap();
931
932 assert_eq!(result.len(), 0);
934 }
935
936 #[test]
937 fn test_consistent_style_with_indented_blocks() {
938 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
939 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
940 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
941 let result = rule.check(&ctx).unwrap();
942
943 assert_eq!(result.len(), 0);
945 }
946
947 #[test]
948 fn test_consistent_style_mixed() {
949 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
950 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
951 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
952 let result = rule.check(&ctx).unwrap();
953
954 assert!(!result.is_empty());
956 }
957
958 #[test]
959 fn test_fenced_style_with_indented_blocks() {
960 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
961 let content = "Text\n\n indented code\n more code\n\nMore text";
962 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
963 let result = rule.check(&ctx).unwrap();
964
965 assert!(!result.is_empty());
967 assert!(result[0].message.contains("Use fenced code blocks"));
968 }
969
970 #[test]
971 fn test_indented_style_with_fenced_blocks() {
972 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
973 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
974 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
975 let result = rule.check(&ctx).unwrap();
976
977 assert!(!result.is_empty());
979 assert!(result[0].message.contains("Use fenced code blocks"));
980 }
981
982 #[test]
983 fn test_unclosed_code_block() {
984 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
985 let content = "```\ncode without closing fence";
986 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
987 let result = rule.check(&ctx).unwrap();
988
989 assert_eq!(result.len(), 1);
990 assert!(result[0].message.contains("never closed"));
991 }
992
993 #[test]
994 fn test_nested_code_blocks() {
995 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
996 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
997 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
998 let result = rule.check(&ctx).unwrap();
999
1000 assert_eq!(result.len(), 0);
1002 }
1003
1004 #[test]
1005 fn test_fix_indented_to_fenced() {
1006 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1007 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
1008 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1009 let fixed = rule.fix(&ctx).unwrap();
1010
1011 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
1012 }
1013
1014 #[test]
1015 fn test_fix_fenced_to_indented() {
1016 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1017 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
1018 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1019 let fixed = rule.fix(&ctx).unwrap();
1020
1021 assert!(fixed.contains(" code line 1\n code line 2"));
1022 assert!(!fixed.contains("```"));
1023 }
1024
1025 #[test]
1026 fn test_fix_unclosed_block() {
1027 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1028 let content = "```\ncode without closing";
1029 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1030 let fixed = rule.fix(&ctx).unwrap();
1031
1032 assert!(fixed.ends_with("```"));
1034 }
1035
1036 #[test]
1037 fn test_code_block_in_list() {
1038 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1039 let content = "- List item\n code in list\n more code\n- Next item";
1040 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1041 let result = rule.check(&ctx).unwrap();
1042
1043 assert_eq!(result.len(), 0);
1045 }
1046
1047 #[test]
1048 fn test_detect_style_fenced() {
1049 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1050 let content = "```\ncode\n```";
1051 let style = rule.detect_style(content);
1052
1053 assert_eq!(style, Some(CodeBlockStyle::Fenced));
1054 }
1055
1056 #[test]
1057 fn test_detect_style_indented() {
1058 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1059 let content = "Text\n\n code\n\nMore";
1060 let style = rule.detect_style(content);
1061
1062 assert_eq!(style, Some(CodeBlockStyle::Indented));
1063 }
1064
1065 #[test]
1066 fn test_detect_style_none() {
1067 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1068 let content = "No code blocks here";
1069 let style = rule.detect_style(content);
1070
1071 assert_eq!(style, None);
1072 }
1073
1074 #[test]
1075 fn test_tilde_fence() {
1076 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1077 let content = "~~~\ncode\n~~~";
1078 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1079 let result = rule.check(&ctx).unwrap();
1080
1081 assert_eq!(result.len(), 0);
1083 }
1084
1085 #[test]
1086 fn test_language_specification() {
1087 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1088 let content = "```rust\nfn main() {}\n```";
1089 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1090 let result = rule.check(&ctx).unwrap();
1091
1092 assert_eq!(result.len(), 0);
1093 }
1094
1095 #[test]
1096 fn test_empty_content() {
1097 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1098 let content = "";
1099 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1100 let result = rule.check(&ctx).unwrap();
1101
1102 assert_eq!(result.len(), 0);
1103 }
1104
1105 #[test]
1106 fn test_default_config() {
1107 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1108 let (name, _config) = rule.default_config_section().unwrap();
1109 assert_eq!(name, "MD046");
1110 }
1111
1112 #[test]
1113 fn test_markdown_documentation_block() {
1114 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1115 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
1116 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1117 let result = rule.check(&ctx).unwrap();
1118
1119 assert_eq!(result.len(), 0);
1121 }
1122
1123 #[test]
1124 fn test_preserve_trailing_newline() {
1125 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1126 let content = "```\ncode\n```\n";
1127 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1128 let fixed = rule.fix(&ctx).unwrap();
1129
1130 assert_eq!(fixed, content);
1131 }
1132}