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::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_footnote_definition(&self, line: &str) -> bool {
64 let trimmed = line.trim_start();
65 if !trimmed.starts_with("[^") || trimmed.len() < 5 {
66 return false;
67 }
68
69 if let Some(close_bracket_pos) = trimmed.find("]:")
70 && close_bracket_pos > 2
71 {
72 let label = &trimmed[2..close_bracket_pos];
73
74 if label.trim().is_empty() {
75 return false;
76 }
77
78 if label.contains('\r') {
80 return false;
81 }
82
83 if label.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
85 return true;
86 }
87 }
88
89 false
90 }
91
92 fn precompute_block_continuation_context(&self, lines: &[&str]) -> Vec<bool> {
115 let mut in_continuation_context = vec![false; lines.len()];
116 let mut last_list_item_line: Option<usize> = None;
117 let mut last_footnote_line: Option<usize> = None;
118 let mut blank_line_count = 0;
119
120 for (i, line) in lines.iter().enumerate() {
121 let trimmed = line.trim_start();
122 let indent_len = line.len() - trimmed.len();
123
124 if self.is_list_item(line) {
126 last_list_item_line = Some(i);
127 last_footnote_line = None; blank_line_count = 0;
129 in_continuation_context[i] = true;
130 continue;
131 }
132
133 if self.is_footnote_definition(line) {
135 last_footnote_line = Some(i);
136 last_list_item_line = None; blank_line_count = 0;
138 in_continuation_context[i] = true;
139 continue;
140 }
141
142 if line.trim().is_empty() {
144 if last_list_item_line.is_some() || last_footnote_line.is_some() {
146 blank_line_count += 1;
147 in_continuation_context[i] = true;
148
149 }
153 continue;
154 }
155
156 if indent_len == 0 && !trimmed.is_empty() {
158 if trimmed.starts_with('#') {
162 last_list_item_line = None;
163 last_footnote_line = None;
164 blank_line_count = 0;
165 continue;
166 }
167
168 if trimmed.starts_with("---") || trimmed.starts_with("***") {
170 last_list_item_line = None;
171 last_footnote_line = None;
172 blank_line_count = 0;
173 continue;
174 }
175
176 if let Some(list_line) = last_list_item_line
179 && (i - list_line > 5 || blank_line_count > 1)
180 {
181 last_list_item_line = None;
182 }
183
184 if last_footnote_line.is_some() {
186 last_footnote_line = None;
187 }
188
189 blank_line_count = 0;
190
191 if last_list_item_line.is_none() && last_footnote_line.is_some() {
193 last_footnote_line = None;
194 }
195 continue;
196 }
197
198 if indent_len > 0 && (last_list_item_line.is_some() || last_footnote_line.is_some()) {
200 in_continuation_context[i] = true;
201 blank_line_count = 0;
202 }
203 }
204
205 in_continuation_context
206 }
207
208 fn is_indented_code_block_with_context(
210 &self,
211 lines: &[&str],
212 i: usize,
213 is_mkdocs: bool,
214 in_list_context: &[bool],
215 in_tab_context: &[bool],
216 ) -> bool {
217 if i >= lines.len() {
218 return false;
219 }
220
221 let line = lines[i];
222
223 if !(line.starts_with(" ") || line.starts_with("\t")) {
225 return false;
226 }
227
228 if in_list_context[i] {
230 return false;
231 }
232
233 if is_mkdocs && in_tab_context[i] {
235 return false;
236 }
237
238 let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
241 let prev_is_indented_code = i > 0
242 && (lines[i - 1].starts_with(" ") || lines[i - 1].starts_with("\t"))
243 && !in_list_context[i - 1]
244 && !(is_mkdocs && in_tab_context[i - 1]);
245
246 if !has_blank_line_before && !prev_is_indented_code {
249 return false;
250 }
251
252 true
253 }
254
255 fn precompute_mkdocs_tab_context(&self, lines: &[&str]) -> Vec<bool> {
257 let mut in_tab_context = vec![false; lines.len()];
258 let mut current_tab_indent: Option<usize> = None;
259
260 for (i, line) in lines.iter().enumerate() {
261 if mkdocs_tabs::is_tab_marker(line) {
263 let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
264 current_tab_indent = Some(tab_indent);
265 in_tab_context[i] = true;
266 continue;
267 }
268
269 if let Some(tab_indent) = current_tab_indent {
271 if mkdocs_tabs::is_tab_content(line, tab_indent) {
272 in_tab_context[i] = true;
273 } else if !line.trim().is_empty() && !line.starts_with(" ") {
274 current_tab_indent = None;
276 } else {
277 in_tab_context[i] = true;
279 }
280 }
281 }
282
283 in_tab_context
284 }
285
286 fn check_unclosed_code_blocks(
287 &self,
288 ctx: &crate::lint_context::LintContext,
289 ) -> Result<Vec<LintWarning>, LintError> {
290 let mut warnings = Vec::new();
291 let lines: Vec<&str> = ctx.content.lines().collect();
292 let mut fence_stack: Vec<(String, usize, usize, bool, bool)> = Vec::new(); let mut inside_markdown_documentation_block = false;
297
298 for (i, line) in lines.iter().enumerate() {
299 let trimmed = line.trim_start();
300
301 if let Some(line_info) = ctx.lines.get(i)
303 && line_info.in_html_comment
304 {
305 continue;
306 }
307
308 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
310 let fence_char = if trimmed.starts_with("```") { '`' } else { '~' };
311
312 let fence_length = trimmed.chars().take_while(|&c| c == fence_char).count();
314
315 let after_fence = &trimmed[fence_length..];
317
318 if fence_char == '`' && after_fence.contains('`') {
322 continue;
323 }
324
325 let is_valid_fence_pattern = if after_fence.is_empty() {
331 true
333 } else if after_fence.starts_with(' ') || after_fence.starts_with('\t') {
334 true
336 } else {
337 let identifier = after_fence.trim().to_lowercase();
340
341 if identifier.contains("fence") || identifier.contains("still") {
343 false
344 } else if identifier.len() > 20 {
345 false
347 } else if let Some(first_char) = identifier.chars().next() {
348 if !first_char.is_alphabetic() && first_char != '#' {
350 false
351 } else {
352 let valid_chars = identifier.chars().all(|c| {
355 c.is_alphanumeric() || c == '-' || c == '_' || c == '+' || c == '#' || c == '.'
356 });
357
358 valid_chars && identifier.len() >= 2
360 }
361 } else {
362 false
363 }
364 };
365
366 if !fence_stack.is_empty() {
368 if !is_valid_fence_pattern {
370 continue;
371 }
372
373 if let Some((open_marker, open_length, _, _, _)) = fence_stack.last() {
375 if fence_char == open_marker.chars().next().unwrap() && fence_length >= *open_length {
376 if !after_fence.trim().is_empty() {
378 let has_special_chars = after_fence.chars().any(|c| {
384 !c.is_alphanumeric()
385 && c != '-'
386 && c != '_'
387 && c != '+'
388 && c != '#'
389 && c != '.'
390 && c != ' '
391 && c != '\t'
392 });
393
394 if has_special_chars {
395 continue; }
397
398 if fence_length > 4 && after_fence.chars().take(4).all(|c| !c.is_alphanumeric()) {
400 continue; }
402
403 if !after_fence.starts_with(' ') && !after_fence.starts_with('\t') {
405 let identifier = after_fence.trim();
406
407 if let Some(first) = identifier.chars().next()
409 && !first.is_alphabetic()
410 && first != '#'
411 {
412 continue;
413 }
414
415 if identifier.len() > 30 {
417 continue;
418 }
419 }
420 }
421 } else {
423 if !after_fence.is_empty()
428 && !after_fence.starts_with(' ')
429 && !after_fence.starts_with('\t')
430 {
431 let identifier = after_fence.trim();
433
434 if identifier.chars().any(|c| {
436 !c.is_alphanumeric() && c != '-' && c != '_' && c != '+' && c != '#' && c != '.'
437 }) {
438 continue;
439 }
440
441 if let Some(first) = identifier.chars().next()
443 && !first.is_alphabetic()
444 && first != '#'
445 {
446 continue;
447 }
448 }
449 }
450 }
451 }
452
453 if let Some((open_marker, open_length, _open_line, _flagged, _is_md)) = fence_stack.last() {
457 if fence_char == open_marker.chars().next().unwrap() && fence_length >= *open_length {
459 let after_fence = &trimmed[fence_length..];
461 if after_fence.trim().is_empty() {
462 let _popped = fence_stack.pop();
464
465 if let Some((_, _, _, _, is_md)) = _popped
467 && is_md
468 {
469 inside_markdown_documentation_block = false;
470 }
471 continue;
472 }
473 }
474 }
475
476 if !after_fence.trim().is_empty() || fence_stack.is_empty() {
479 let has_nested_issue =
483 if let Some((open_marker, open_length, open_line, _, _)) = fence_stack.last_mut() {
484 if fence_char == open_marker.chars().next().unwrap()
485 && fence_length >= *open_length
486 && !inside_markdown_documentation_block
487 {
488 let (opening_start_line, opening_start_col, opening_end_line, opening_end_col) =
490 calculate_line_range(*open_line, lines[*open_line - 1]);
491
492 let line_start_byte = ctx.line_index.get_line_start_byte(i + 1).unwrap_or(0);
494
495 warnings.push(LintWarning {
496 rule_name: Some(self.name().to_string()),
497 line: opening_start_line,
498 column: opening_start_col,
499 end_line: opening_end_line,
500 end_column: opening_end_col,
501 message: format!(
502 "Code block '{}' should be closed before starting new one at line {}",
503 open_marker,
504 i + 1
505 ),
506 severity: Severity::Warning,
507 fix: Some(Fix {
508 range: (line_start_byte..line_start_byte),
509 replacement: format!("{open_marker}\n\n"),
510 }),
511 });
512
513 fence_stack.last_mut().unwrap().3 = true;
515 true } else {
517 false
518 }
519 } else {
520 false
521 };
522
523 let after_fence_for_lang = &trimmed[fence_length..];
525 let lang_info = after_fence_for_lang.trim().to_lowercase();
526 let is_markdown_fence = lang_info.starts_with("markdown") || lang_info.starts_with("md");
527
528 if is_markdown_fence && !inside_markdown_documentation_block {
530 inside_markdown_documentation_block = true;
531 }
532
533 let fence_marker = fence_char.to_string().repeat(fence_length);
535 fence_stack.push((fence_marker, fence_length, i + 1, has_nested_issue, is_markdown_fence));
536 }
537 }
538 }
539
540 for (fence_marker, _, opening_line, flagged_for_nested, _) in fence_stack {
543 if !flagged_for_nested {
544 let (start_line, start_col, end_line, end_col) =
545 calculate_line_range(opening_line, lines[opening_line - 1]);
546
547 warnings.push(LintWarning {
548 rule_name: Some(self.name().to_string()),
549 line: start_line,
550 column: start_col,
551 end_line,
552 end_column: end_col,
553 message: format!("Code block opened with '{fence_marker}' but never closed"),
554 severity: Severity::Warning,
555 fix: Some(Fix {
556 range: (ctx.content.len()..ctx.content.len()),
557 replacement: format!("\n{fence_marker}"),
558 }),
559 });
560 }
561 }
562
563 Ok(warnings)
564 }
565
566 fn detect_style(&self, content: &str, is_mkdocs: bool) -> Option<CodeBlockStyle> {
567 if content.is_empty() {
569 return None;
570 }
571
572 let lines: Vec<&str> = content.lines().collect();
573 let mut fenced_count = 0;
574 let mut indented_count = 0;
575
576 let in_list_context = self.precompute_block_continuation_context(&lines);
578 let in_tab_context = if is_mkdocs {
579 self.precompute_mkdocs_tab_context(&lines)
580 } else {
581 vec![false; lines.len()]
582 };
583
584 let mut in_fenced = false;
586 let mut prev_was_indented = false;
587
588 for (i, line) in lines.iter().enumerate() {
589 if self.is_fenced_code_block_start(line) {
590 if !in_fenced {
591 fenced_count += 1;
593 in_fenced = true;
594 } else {
595 in_fenced = false;
597 }
598 } else if !in_fenced
599 && self.is_indented_code_block_with_context(&lines, i, is_mkdocs, &in_list_context, &in_tab_context)
600 {
601 if !prev_was_indented {
603 indented_count += 1;
604 }
605 prev_was_indented = true;
606 } else {
607 prev_was_indented = false;
608 }
609 }
610
611 if fenced_count == 0 && indented_count == 0 {
612 None
614 } else if fenced_count > 0 && indented_count == 0 {
615 Some(CodeBlockStyle::Fenced)
617 } else if fenced_count == 0 && indented_count > 0 {
618 Some(CodeBlockStyle::Indented)
620 } else {
621 if fenced_count >= indented_count {
624 Some(CodeBlockStyle::Fenced)
625 } else {
626 Some(CodeBlockStyle::Indented)
627 }
628 }
629 }
630}
631
632impl Rule for MD046CodeBlockStyle {
633 fn name(&self) -> &'static str {
634 "MD046"
635 }
636
637 fn description(&self) -> &'static str {
638 "Code blocks should use a consistent style"
639 }
640
641 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
642 if ctx.content.is_empty() {
644 return Ok(Vec::new());
645 }
646
647 if !ctx.content.contains("```") && !ctx.content.contains("~~~") && !ctx.content.contains(" ") {
649 return Ok(Vec::new());
650 }
651
652 let unclosed_warnings = self.check_unclosed_code_blocks(ctx)?;
654
655 if !unclosed_warnings.is_empty() {
657 return Ok(unclosed_warnings);
658 }
659
660 let lines: Vec<&str> = ctx.content.lines().collect();
662 let mut warnings = Vec::new();
663
664 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
666
667 let in_list_context = self.precompute_block_continuation_context(&lines);
669 let in_tab_context = if is_mkdocs {
670 self.precompute_mkdocs_tab_context(&lines)
671 } else {
672 vec![false; lines.len()]
673 };
674
675 let target_style = match self.config.style {
677 CodeBlockStyle::Consistent => self
678 .detect_style(ctx.content, is_mkdocs)
679 .unwrap_or(CodeBlockStyle::Fenced),
680 _ => self.config.style,
681 };
682
683 let mut in_fenced_block = vec![false; lines.len()];
687 for &(start, end) in &ctx.code_blocks {
688 if start < ctx.content.len() && end <= ctx.content.len() {
690 let block_content = &ctx.content[start..end];
691 let is_fenced = block_content.starts_with("```") || block_content.starts_with("~~~");
692
693 if is_fenced {
694 for (line_idx, line_info) in ctx.lines.iter().enumerate() {
696 if line_info.byte_offset >= start && line_info.byte_offset < end {
697 in_fenced_block[line_idx] = true;
698 }
699 }
700 }
701 }
702 }
703
704 let mut in_fence = false;
705 for (i, line) in lines.iter().enumerate() {
706 let trimmed = line.trim_start();
707
708 if ctx.line_info(i + 1).is_some_and(|info| info.in_html_block) {
710 continue;
711 }
712
713 if ctx.line_info(i + 1).is_some_and(|info| info.in_html_comment) {
715 continue;
716 }
717
718 if ctx.lines[i].in_mkdocstrings {
721 continue;
722 }
723
724 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
726 if target_style == CodeBlockStyle::Indented && !in_fence {
727 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
730 warnings.push(LintWarning {
731 rule_name: Some(self.name().to_string()),
732 line: start_line,
733 column: start_col,
734 end_line,
735 end_column: end_col,
736 message: "Use indented code blocks".to_string(),
737 severity: Severity::Warning,
738 fix: Some(Fix {
739 range: ctx.line_index.line_col_to_byte_range(i + 1, 1),
740 replacement: String::new(),
741 }),
742 });
743 }
744 in_fence = !in_fence;
746 continue;
747 }
748
749 if in_fenced_block[i] {
752 continue;
753 }
754
755 if self.is_indented_code_block_with_context(&lines, i, is_mkdocs, &in_list_context, &in_tab_context)
757 && target_style == CodeBlockStyle::Fenced
758 {
759 let prev_line_is_indented = i > 0
761 && self.is_indented_code_block_with_context(
762 &lines,
763 i - 1,
764 is_mkdocs,
765 &in_list_context,
766 &in_tab_context,
767 );
768
769 if !prev_line_is_indented {
770 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
771 warnings.push(LintWarning {
772 rule_name: Some(self.name().to_string()),
773 line: start_line,
774 column: start_col,
775 end_line,
776 end_column: end_col,
777 message: "Use fenced code blocks".to_string(),
778 severity: Severity::Warning,
779 fix: Some(Fix {
780 range: ctx.line_index.line_col_to_byte_range(i + 1, 1),
781 replacement: format!("```\n{}", line.trim_start()),
782 }),
783 });
784 }
785 }
786 }
787
788 Ok(warnings)
789 }
790
791 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
792 let content = ctx.content;
793 if content.is_empty() {
794 return Ok(String::new());
795 }
796
797 let unclosed_warnings = self.check_unclosed_code_blocks(ctx)?;
799
800 if !unclosed_warnings.is_empty() {
802 for warning in &unclosed_warnings {
804 if warning
805 .message
806 .contains("should be closed before starting new one at line")
807 {
808 if let Some(fix) = &warning.fix {
810 let mut result = String::new();
811 result.push_str(&content[..fix.range.start]);
812 result.push_str(&fix.replacement);
813 result.push_str(&content[fix.range.start..]);
814 return Ok(result);
815 }
816 }
817 }
818 }
819
820 let lines: Vec<&str> = content.lines().collect();
821
822 let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
824 let target_style = match self.config.style {
825 CodeBlockStyle::Consistent => self.detect_style(content, is_mkdocs).unwrap_or(CodeBlockStyle::Fenced),
826 _ => self.config.style,
827 };
828
829 let in_list_context = self.precompute_block_continuation_context(&lines);
831 let in_tab_context = if is_mkdocs {
832 self.precompute_mkdocs_tab_context(&lines)
833 } else {
834 vec![false; lines.len()]
835 };
836
837 let mut result = String::with_capacity(content.len());
838 let mut in_fenced_block = false;
839 let mut fenced_fence_type = None;
840 let mut in_indented_block = false;
841
842 for (i, line) in lines.iter().enumerate() {
843 let trimmed = line.trim_start();
844
845 if !in_fenced_block && (trimmed.starts_with("```") || trimmed.starts_with("~~~")) {
847 in_fenced_block = true;
848 fenced_fence_type = Some(if trimmed.starts_with("```") { "```" } else { "~~~" });
849
850 if target_style == CodeBlockStyle::Indented {
851 in_indented_block = true;
853 } else {
854 result.push_str(line);
856 result.push('\n');
857 }
858 } else if in_fenced_block && fenced_fence_type.is_some() {
859 let fence = fenced_fence_type.unwrap();
860 if trimmed.starts_with(fence) {
861 in_fenced_block = false;
862 fenced_fence_type = None;
863 in_indented_block = false;
864
865 if target_style == CodeBlockStyle::Indented {
866 } else {
868 result.push_str(line);
870 result.push('\n');
871 }
872 } else if target_style == CodeBlockStyle::Indented {
873 result.push_str(" ");
875 result.push_str(trimmed);
876 result.push('\n');
877 } else {
878 result.push_str(line);
880 result.push('\n');
881 }
882 } else if self.is_indented_code_block_with_context(&lines, i, is_mkdocs, &in_list_context, &in_tab_context)
883 {
884 let prev_line_is_indented = i > 0
888 && self.is_indented_code_block_with_context(
889 &lines,
890 i - 1,
891 is_mkdocs,
892 &in_list_context,
893 &in_tab_context,
894 );
895
896 if target_style == CodeBlockStyle::Fenced {
897 if !prev_line_is_indented && !in_indented_block {
898 result.push_str("```\n");
900 result.push_str(line.trim_start());
901 result.push('\n');
902 in_indented_block = true;
903 } else {
904 result.push_str(line.trim_start());
906 result.push('\n');
907 }
908
909 let _next_line_is_indented = i < lines.len() - 1
911 && self.is_indented_code_block_with_context(
912 &lines,
913 i + 1,
914 is_mkdocs,
915 &in_list_context,
916 &in_tab_context,
917 );
918 if !_next_line_is_indented && in_indented_block {
919 result.push_str("```\n");
920 in_indented_block = false;
921 }
922 } else {
923 result.push_str(line);
925 result.push('\n');
926 }
927 } else {
928 if in_indented_block && target_style == CodeBlockStyle::Fenced {
930 result.push_str("```\n");
931 in_indented_block = false;
932 }
933
934 result.push_str(line);
935 result.push('\n');
936 }
937 }
938
939 if in_indented_block && target_style == CodeBlockStyle::Fenced {
941 result.push_str("```\n");
942 }
943
944 if let Some(fence_type) = fenced_fence_type
946 && in_fenced_block
947 {
948 result.push_str(fence_type);
949 result.push('\n');
950 }
951
952 if !content.ends_with('\n') && result.ends_with('\n') {
954 result.pop();
955 }
956
957 Ok(result)
958 }
959
960 fn category(&self) -> RuleCategory {
962 RuleCategory::CodeBlock
963 }
964
965 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
967 ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
970 }
971
972 fn as_any(&self) -> &dyn std::any::Any {
973 self
974 }
975
976 fn default_config_section(&self) -> Option<(String, toml::Value)> {
977 let json_value = serde_json::to_value(&self.config).ok()?;
978 Some((
979 self.name().to_string(),
980 crate::rule_config_serde::json_to_toml_value(&json_value)?,
981 ))
982 }
983
984 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
985 where
986 Self: Sized,
987 {
988 let rule_config = crate::rule_config_serde::load_rule_config::<MD046Config>(config);
989 Box::new(Self::from_config_struct(rule_config))
990 }
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996 use crate::lint_context::LintContext;
997
998 #[test]
999 fn test_fenced_code_block_detection() {
1000 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1001 assert!(rule.is_fenced_code_block_start("```"));
1002 assert!(rule.is_fenced_code_block_start("```rust"));
1003 assert!(rule.is_fenced_code_block_start("~~~"));
1004 assert!(rule.is_fenced_code_block_start("~~~python"));
1005 assert!(rule.is_fenced_code_block_start(" ```"));
1006 assert!(!rule.is_fenced_code_block_start("``"));
1007 assert!(!rule.is_fenced_code_block_start("~~"));
1008 assert!(!rule.is_fenced_code_block_start("Regular text"));
1009 }
1010
1011 #[test]
1012 fn test_consistent_style_with_fenced_blocks() {
1013 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1014 let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
1015 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1016 let result = rule.check(&ctx).unwrap();
1017
1018 assert_eq!(result.len(), 0);
1020 }
1021
1022 #[test]
1023 fn test_consistent_style_with_indented_blocks() {
1024 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1025 let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
1026 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1027 let result = rule.check(&ctx).unwrap();
1028
1029 assert_eq!(result.len(), 0);
1031 }
1032
1033 #[test]
1034 fn test_consistent_style_mixed() {
1035 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1036 let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
1037 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1038 let result = rule.check(&ctx).unwrap();
1039
1040 assert!(!result.is_empty());
1042 }
1043
1044 #[test]
1045 fn test_fenced_style_with_indented_blocks() {
1046 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1047 let content = "Text\n\n indented code\n more code\n\nMore text";
1048 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1049 let result = rule.check(&ctx).unwrap();
1050
1051 assert!(!result.is_empty());
1053 assert!(result[0].message.contains("Use fenced code blocks"));
1054 }
1055
1056 #[test]
1057 fn test_indented_style_with_fenced_blocks() {
1058 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1059 let content = "Text\n\n```\nfenced code\n```\n\nMore text";
1060 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1061 let result = rule.check(&ctx).unwrap();
1062
1063 assert!(!result.is_empty());
1065 assert!(result[0].message.contains("Use indented code blocks"));
1066 }
1067
1068 #[test]
1069 fn test_unclosed_code_block() {
1070 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1071 let content = "```\ncode without closing fence";
1072 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1073 let result = rule.check(&ctx).unwrap();
1074
1075 assert_eq!(result.len(), 1);
1076 assert!(result[0].message.contains("never closed"));
1077 }
1078
1079 #[test]
1080 fn test_nested_code_blocks() {
1081 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1082 let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
1083 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1084 let result = rule.check(&ctx).unwrap();
1085
1086 assert_eq!(result.len(), 0);
1088 }
1089
1090 #[test]
1091 fn test_fix_indented_to_fenced() {
1092 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1093 let content = "Text\n\n code line 1\n code line 2\n\nMore text";
1094 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1095 let fixed = rule.fix(&ctx).unwrap();
1096
1097 assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
1098 }
1099
1100 #[test]
1101 fn test_fix_fenced_to_indented() {
1102 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
1103 let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
1104 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1105 let fixed = rule.fix(&ctx).unwrap();
1106
1107 assert!(fixed.contains(" code line 1\n code line 2"));
1108 assert!(!fixed.contains("```"));
1109 }
1110
1111 #[test]
1112 fn test_fix_unclosed_block() {
1113 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1114 let content = "```\ncode without closing";
1115 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1116 let fixed = rule.fix(&ctx).unwrap();
1117
1118 assert!(fixed.ends_with("```"));
1120 }
1121
1122 #[test]
1123 fn test_code_block_in_list() {
1124 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1125 let content = "- List item\n code in list\n more code\n- Next item";
1126 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1127 let result = rule.check(&ctx).unwrap();
1128
1129 assert_eq!(result.len(), 0);
1131 }
1132
1133 #[test]
1134 fn test_detect_style_fenced() {
1135 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1136 let content = "```\ncode\n```";
1137 let style = rule.detect_style(content, false);
1138
1139 assert_eq!(style, Some(CodeBlockStyle::Fenced));
1140 }
1141
1142 #[test]
1143 fn test_detect_style_indented() {
1144 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1145 let content = "Text\n\n code\n\nMore";
1146 let style = rule.detect_style(content, false);
1147
1148 assert_eq!(style, Some(CodeBlockStyle::Indented));
1149 }
1150
1151 #[test]
1152 fn test_detect_style_none() {
1153 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1154 let content = "No code blocks here";
1155 let style = rule.detect_style(content, false);
1156
1157 assert_eq!(style, None);
1158 }
1159
1160 #[test]
1161 fn test_tilde_fence() {
1162 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1163 let content = "~~~\ncode\n~~~";
1164 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1165 let result = rule.check(&ctx).unwrap();
1166
1167 assert_eq!(result.len(), 0);
1169 }
1170
1171 #[test]
1172 fn test_language_specification() {
1173 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1174 let content = "```rust\nfn main() {}\n```";
1175 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1176 let result = rule.check(&ctx).unwrap();
1177
1178 assert_eq!(result.len(), 0);
1179 }
1180
1181 #[test]
1182 fn test_empty_content() {
1183 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1184 let content = "";
1185 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1186 let result = rule.check(&ctx).unwrap();
1187
1188 assert_eq!(result.len(), 0);
1189 }
1190
1191 #[test]
1192 fn test_default_config() {
1193 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1194 let (name, _config) = rule.default_config_section().unwrap();
1195 assert_eq!(name, "MD046");
1196 }
1197
1198 #[test]
1199 fn test_markdown_documentation_block() {
1200 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1201 let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
1202 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1203 let result = rule.check(&ctx).unwrap();
1204
1205 assert_eq!(result.len(), 0);
1207 }
1208
1209 #[test]
1210 fn test_preserve_trailing_newline() {
1211 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1212 let content = "```\ncode\n```\n";
1213 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1214 let fixed = rule.fix(&ctx).unwrap();
1215
1216 assert_eq!(fixed, content);
1217 }
1218
1219 #[test]
1220 fn test_mkdocs_tabs_not_flagged_as_indented_code() {
1221 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1222 let content = r#"# Document
1223
1224=== "Python"
1225
1226 This is tab content
1227 Not an indented code block
1228
1229 ```python
1230 def hello():
1231 print("Hello")
1232 ```
1233
1234=== "JavaScript"
1235
1236 More tab content here
1237 Also not an indented code block"#;
1238
1239 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1240 let result = rule.check(&ctx).unwrap();
1241
1242 assert_eq!(result.len(), 0);
1244 }
1245
1246 #[test]
1247 fn test_mkdocs_tabs_with_actual_indented_code() {
1248 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1249 let content = r#"# Document
1250
1251=== "Tab 1"
1252
1253 This is tab content
1254
1255Regular text
1256
1257 This is an actual indented code block
1258 Should be flagged"#;
1259
1260 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1261 let result = rule.check(&ctx).unwrap();
1262
1263 assert_eq!(result.len(), 1);
1265 assert!(result[0].message.contains("Use fenced code blocks"));
1266 }
1267
1268 #[test]
1269 fn test_mkdocs_tabs_detect_style() {
1270 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
1271 let content = r#"=== "Tab 1"
1272
1273 Content in tab
1274 More content
1275
1276=== "Tab 2"
1277
1278 Content in second tab"#;
1279
1280 let style = rule.detect_style(content, true);
1282 assert_eq!(style, None); let style = rule.detect_style(content, false);
1286 assert_eq!(style, Some(CodeBlockStyle::Indented));
1287 }
1288
1289 #[test]
1290 fn test_mkdocs_nested_tabs() {
1291 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1292 let content = r#"# Document
1293
1294=== "Outer Tab"
1295
1296 Some content
1297
1298 === "Nested Tab"
1299
1300 Nested tab content
1301 Should not be flagged"#;
1302
1303 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1304 let result = rule.check(&ctx).unwrap();
1305
1306 assert_eq!(result.len(), 0);
1308 }
1309
1310 #[test]
1311 fn test_footnote_indented_paragraphs_not_flagged() {
1312 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1313 let content = r#"# Test Document with Footnotes
1314
1315This is some text with a footnote[^1].
1316
1317Here's some code:
1318
1319```bash
1320echo "fenced code block"
1321```
1322
1323More text with another footnote[^2].
1324
1325[^1]: Really interesting footnote text.
1326
1327 Even more interesting second paragraph.
1328
1329[^2]: Another footnote.
1330
1331 With a second paragraph too.
1332
1333 And even a third paragraph!"#;
1334
1335 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1336 let result = rule.check(&ctx).unwrap();
1337
1338 assert_eq!(result.len(), 0);
1340 }
1341
1342 #[test]
1343 fn test_footnote_definition_detection() {
1344 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1345
1346 assert!(rule.is_footnote_definition("[^1]: Footnote text"));
1349 assert!(rule.is_footnote_definition("[^foo]: Footnote text"));
1350 assert!(rule.is_footnote_definition("[^long-name]: Footnote text"));
1351 assert!(rule.is_footnote_definition("[^test_123]: Mixed chars"));
1352 assert!(rule.is_footnote_definition(" [^1]: Indented footnote"));
1353 assert!(rule.is_footnote_definition("[^a]: Minimal valid footnote"));
1354 assert!(rule.is_footnote_definition("[^123]: Numeric label"));
1355 assert!(rule.is_footnote_definition("[^_]: Single underscore"));
1356 assert!(rule.is_footnote_definition("[^-]: Single hyphen"));
1357
1358 assert!(!rule.is_footnote_definition("[^]: No label"));
1360 assert!(!rule.is_footnote_definition("[^ ]: Whitespace only"));
1361 assert!(!rule.is_footnote_definition("[^ ]: Multiple spaces"));
1362 assert!(!rule.is_footnote_definition("[^\t]: Tab only"));
1363
1364 assert!(!rule.is_footnote_definition("[^]]: Extra bracket"));
1366 assert!(!rule.is_footnote_definition("Regular text [^1]:"));
1367 assert!(!rule.is_footnote_definition("[1]: Not a footnote"));
1368 assert!(!rule.is_footnote_definition("[^")); assert!(!rule.is_footnote_definition("[^1:")); assert!(!rule.is_footnote_definition("^1]: Missing opening bracket"));
1371
1372 assert!(!rule.is_footnote_definition("[^test.name]: Period"));
1374 assert!(!rule.is_footnote_definition("[^test name]: Space in label"));
1375 assert!(!rule.is_footnote_definition("[^test@name]: Special char"));
1376 assert!(!rule.is_footnote_definition("[^test/name]: Slash"));
1377 assert!(!rule.is_footnote_definition("[^test\\name]: Backslash"));
1378
1379 assert!(!rule.is_footnote_definition("[^test\r]: Carriage return"));
1382 }
1383
1384 #[test]
1385 fn test_footnote_with_blank_lines() {
1386 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1390 let content = r#"# Document
1391
1392Text with footnote[^1].
1393
1394[^1]: First paragraph.
1395
1396 Second paragraph after blank line.
1397
1398 Third paragraph after another blank line.
1399
1400Regular text at column 0 ends the footnote."#;
1401
1402 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1403 let result = rule.check(&ctx).unwrap();
1404
1405 assert_eq!(
1407 result.len(),
1408 0,
1409 "Indented content within footnotes should not trigger MD046"
1410 );
1411 }
1412
1413 #[test]
1414 fn test_footnote_multiple_consecutive_blank_lines() {
1415 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1418 let content = r#"Text[^1].
1419
1420[^1]: First paragraph.
1421
1422
1423
1424 Content after three blank lines (still part of footnote).
1425
1426Not indented, so footnote ends here."#;
1427
1428 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1429 let result = rule.check(&ctx).unwrap();
1430
1431 assert_eq!(
1433 result.len(),
1434 0,
1435 "Multiple blank lines shouldn't break footnote continuation"
1436 );
1437 }
1438
1439 #[test]
1440 fn test_footnote_terminated_by_non_indented_content() {
1441 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1444 let content = r#"[^1]: Footnote content.
1445
1446 More indented content in footnote.
1447
1448This paragraph is not indented, so footnote ends.
1449
1450 This should be flagged as indented code block."#;
1451
1452 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1453 let result = rule.check(&ctx).unwrap();
1454
1455 assert_eq!(
1457 result.len(),
1458 1,
1459 "Indented code after footnote termination should be flagged"
1460 );
1461 assert!(
1462 result[0].message.contains("Use fenced code blocks"),
1463 "Expected MD046 warning for indented code block"
1464 );
1465 assert!(result[0].line >= 7, "Warning should be on the indented code block line");
1466 }
1467
1468 #[test]
1469 fn test_footnote_terminated_by_structural_elements() {
1470 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1472 let content = r#"[^1]: Footnote content.
1473
1474 More content.
1475
1476## Heading terminates footnote
1477
1478 This indented content should be flagged.
1479
1480---
1481
1482 This should also be flagged (after horizontal rule)."#;
1483
1484 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1485 let result = rule.check(&ctx).unwrap();
1486
1487 assert_eq!(
1489 result.len(),
1490 2,
1491 "Both indented blocks after termination should be flagged"
1492 );
1493 }
1494
1495 #[test]
1496 fn test_footnote_with_code_block_inside() {
1497 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1500 let content = r#"Text[^1].
1501
1502[^1]: Footnote with code:
1503
1504 ```python
1505 def hello():
1506 print("world")
1507 ```
1508
1509 More footnote text after code."#;
1510
1511 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1512 let result = rule.check(&ctx).unwrap();
1513
1514 assert_eq!(result.len(), 0, "Fenced code blocks within footnotes should be allowed");
1516 }
1517
1518 #[test]
1519 fn test_footnote_with_8_space_indented_code() {
1520 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1523 let content = r#"Text[^1].
1524
1525[^1]: Footnote with nested code.
1526
1527 code block
1528 more code"#;
1529
1530 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1531 let result = rule.check(&ctx).unwrap();
1532
1533 assert_eq!(
1535 result.len(),
1536 0,
1537 "8-space indented code within footnotes represents nested code blocks"
1538 );
1539 }
1540
1541 #[test]
1542 fn test_multiple_footnotes() {
1543 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1546 let content = r#"Text[^1] and more[^2].
1547
1548[^1]: First footnote.
1549
1550 Continuation of first.
1551
1552[^2]: Second footnote starts here, ending the first.
1553
1554 Continuation of second."#;
1555
1556 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1557 let result = rule.check(&ctx).unwrap();
1558
1559 assert_eq!(
1561 result.len(),
1562 0,
1563 "Multiple footnotes should each maintain their continuation context"
1564 );
1565 }
1566
1567 #[test]
1568 fn test_list_item_ends_footnote_context() {
1569 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1571 let content = r#"[^1]: Footnote.
1572
1573 Content in footnote.
1574
1575- List item starts here (ends footnote context).
1576
1577 This indented content is part of the list, not the footnote."#;
1578
1579 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1580 let result = rule.check(&ctx).unwrap();
1581
1582 assert_eq!(
1584 result.len(),
1585 0,
1586 "List items should end footnote context and start their own"
1587 );
1588 }
1589
1590 #[test]
1591 fn test_footnote_vs_actual_indented_code() {
1592 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1595 let content = r#"# Heading
1596
1597Text with footnote[^1].
1598
1599[^1]: Footnote content.
1600
1601 Part of footnote (should not be flagged).
1602
1603Regular paragraph ends footnote context.
1604
1605 This is actual indented code (MUST be flagged)
1606 Should be detected as code block"#;
1607
1608 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1609 let result = rule.check(&ctx).unwrap();
1610
1611 assert_eq!(
1613 result.len(),
1614 1,
1615 "Must still detect indented code blocks outside footnotes"
1616 );
1617 assert!(
1618 result[0].message.contains("Use fenced code blocks"),
1619 "Expected MD046 warning for indented code"
1620 );
1621 assert!(
1622 result[0].line >= 11,
1623 "Warning should be on the actual indented code line"
1624 );
1625 }
1626
1627 #[test]
1628 fn test_spec_compliant_label_characters() {
1629 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1632
1633 assert!(rule.is_footnote_definition("[^test]: text"));
1635 assert!(rule.is_footnote_definition("[^TEST]: text"));
1636 assert!(rule.is_footnote_definition("[^test-name]: text"));
1637 assert!(rule.is_footnote_definition("[^test_name]: text"));
1638 assert!(rule.is_footnote_definition("[^test123]: text"));
1639 assert!(rule.is_footnote_definition("[^123]: text"));
1640 assert!(rule.is_footnote_definition("[^a1b2c3]: text"));
1641
1642 assert!(!rule.is_footnote_definition("[^test.name]: text")); assert!(!rule.is_footnote_definition("[^test name]: text")); assert!(!rule.is_footnote_definition("[^test@name]: text")); assert!(!rule.is_footnote_definition("[^test#name]: text")); assert!(!rule.is_footnote_definition("[^test$name]: text")); assert!(!rule.is_footnote_definition("[^test%name]: text")); }
1650
1651 #[test]
1652 fn test_code_block_inside_html_comment() {
1653 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1656 let content = r#"# Document
1657
1658Some text.
1659
1660<!--
1661Example code block in comment:
1662
1663```typescript
1664console.log("Hello");
1665```
1666
1667More comment text.
1668-->
1669
1670More content."#;
1671
1672 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1673 let result = rule.check(&ctx).unwrap();
1674
1675 assert_eq!(
1676 result.len(),
1677 0,
1678 "Code blocks inside HTML comments should not be flagged as unclosed"
1679 );
1680 }
1681
1682 #[test]
1683 fn test_unclosed_fence_inside_html_comment() {
1684 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1686 let content = r#"# Document
1687
1688<!--
1689Example with intentionally unclosed fence:
1690
1691```
1692code without closing
1693-->
1694
1695More content."#;
1696
1697 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1698 let result = rule.check(&ctx).unwrap();
1699
1700 assert_eq!(
1701 result.len(),
1702 0,
1703 "Unclosed fences inside HTML comments should be ignored"
1704 );
1705 }
1706
1707 #[test]
1708 fn test_multiline_html_comment_with_indented_code() {
1709 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1711 let content = r#"# Document
1712
1713<!--
1714Example:
1715
1716 indented code
1717 more code
1718
1719End of comment.
1720-->
1721
1722Regular text."#;
1723
1724 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1725 let result = rule.check(&ctx).unwrap();
1726
1727 assert_eq!(
1728 result.len(),
1729 0,
1730 "Indented code inside HTML comments should not be flagged"
1731 );
1732 }
1733
1734 #[test]
1735 fn test_code_block_after_html_comment() {
1736 let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
1738 let content = r#"# Document
1739
1740<!-- comment -->
1741
1742Text before.
1743
1744 indented code should be flagged
1745
1746More text."#;
1747
1748 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1749 let result = rule.check(&ctx).unwrap();
1750
1751 assert_eq!(
1752 result.len(),
1753 1,
1754 "Code blocks after HTML comments should still be detected"
1755 );
1756 assert!(result[0].message.contains("Use fenced code blocks"));
1757 }
1758}