1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, Severity};
2
3#[derive(Clone, Default)]
10pub struct MD065BlanksAroundHorizontalRules;
11
12impl MD065BlanksAroundHorizontalRules {
13 fn is_blank_line(line: &str) -> bool {
15 line.trim().is_empty()
16 }
17
18 fn is_horizontal_rule(line: &str) -> bool {
20 let leading_spaces = line.len() - line.trim_start_matches(' ').len();
22 if leading_spaces > 3 || line.starts_with('\t') {
23 return false;
24 }
25
26 let trimmed = line.trim();
27 if trimmed.len() < 3 {
28 return false;
29 }
30
31 let chars: Vec<char> = trimmed.chars().collect();
33 let first_non_space = chars.iter().find(|&&c| c != ' ');
34
35 if let Some(&marker) = first_non_space {
36 if marker != '-' && marker != '*' && marker != '_' {
37 return false;
38 }
39
40 let marker_count = chars.iter().filter(|&&c| c == marker).count();
42 let other_count = chars.iter().filter(|&&c| c != marker && c != ' ').count();
43
44 marker_count >= 3 && other_count == 0
46 } else {
47 false
48 }
49 }
50
51 fn is_setext_heading_marker(lines: &[&str], line_index: usize) -> bool {
53 if line_index == 0 {
54 return false;
55 }
56
57 let line = lines[line_index].trim();
58 let prev_line = lines[line_index - 1].trim();
59
60 if prev_line.is_empty() {
64 return false;
65 }
66
67 let has_hyphen = line.contains('-');
70 let has_equals = line.contains('=');
71
72 if has_hyphen == has_equals {
74 return false; }
76
77 let marker = if has_hyphen { '-' } else { '=' };
78
79 let trimmed = line.trim();
82 trimmed.chars().all(|c| c == marker)
83 }
84
85 fn count_blank_lines_before(lines: &[&str], line_index: usize) -> usize {
87 let mut count = 0;
88 let mut i = line_index;
89 while i > 0 {
90 i -= 1;
91 if Self::is_blank_line(lines[i]) {
92 count += 1;
93 } else {
94 break;
95 }
96 }
97 count
98 }
99
100 fn count_blank_lines_after(lines: &[&str], line_index: usize) -> usize {
102 let mut count = 0;
103 let mut i = line_index + 1;
104 while i < lines.len() {
105 if Self::is_blank_line(lines[i]) {
106 count += 1;
107 i += 1;
108 } else {
109 break;
110 }
111 }
112 count
113 }
114}
115
116impl Rule for MD065BlanksAroundHorizontalRules {
117 fn name(&self) -> &'static str {
118 "MD065"
119 }
120
121 fn description(&self) -> &'static str {
122 "Horizontal rules should be surrounded by blank lines"
123 }
124
125 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
126 let content = ctx.content;
127 let line_index = &ctx.line_index;
128 let mut warnings = Vec::new();
129
130 if content.is_empty() {
131 return Ok(Vec::new());
132 }
133
134 let lines: Vec<&str> = content.lines().collect();
135
136 for (i, line) in lines.iter().enumerate() {
137 if let Some(line_info) = ctx.lines.get(i)
139 && (line_info.in_code_block || line_info.in_front_matter)
140 {
141 continue;
142 }
143
144 if !Self::is_horizontal_rule(line) {
145 continue;
146 }
147
148 if Self::is_setext_heading_marker(&lines, i) {
150 continue;
151 }
152
153 if i > 0 && Self::count_blank_lines_before(&lines, i) == 0 {
155 warnings.push(LintWarning {
156 rule_name: Some(self.name().to_string()),
157 message: "Missing blank line before horizontal rule".to_string(),
158 line: i + 1,
159 column: 1,
160 end_line: i + 1,
161 end_column: 2,
162 severity: Severity::Warning,
163 fix: Some(Fix {
164 range: line_index.line_col_to_byte_range(i + 1, 1),
165 replacement: "\n".to_string(),
166 }),
167 });
168 }
169
170 if i < lines.len() - 1 && Self::count_blank_lines_after(&lines, i) == 0 {
172 warnings.push(LintWarning {
173 rule_name: Some(self.name().to_string()),
174 message: "Missing blank line after horizontal rule".to_string(),
175 line: i + 1,
176 column: lines[i].len() + 1,
177 end_line: i + 1,
178 end_column: lines[i].len() + 2,
179 severity: Severity::Warning,
180 fix: Some(Fix {
181 range: line_index.line_col_to_byte_range(i + 1, lines[i].len() + 1),
182 replacement: "\n".to_string(),
183 }),
184 });
185 }
186 }
187
188 Ok(warnings)
189 }
190
191 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
192 let content = ctx.content;
193
194 let mut warnings = self.check(ctx)?;
195 if warnings.is_empty() {
196 return Ok(content.to_string());
197 }
198
199 let lines: Vec<&str> = content.lines().collect();
200 let mut result = Vec::new();
201
202 for (i, line) in lines.iter().enumerate() {
203 let warning_before = warnings
205 .iter()
206 .position(|w| w.line == i + 1 && w.message.contains("before horizontal rule"));
207
208 if let Some(idx) = warning_before {
209 result.push("".to_string());
210 warnings.remove(idx);
211 }
212
213 result.push((*line).to_string());
214
215 let warning_after = warnings
217 .iter()
218 .position(|w| w.line == i + 1 && w.message.contains("after horizontal rule"));
219
220 if let Some(idx) = warning_after {
221 result.push("".to_string());
222 warnings.remove(idx);
223 }
224 }
225
226 Ok(result.join("\n"))
227 }
228
229 fn as_any(&self) -> &dyn std::any::Any {
230 self
231 }
232
233 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
234 where
235 Self: Sized,
236 {
237 Box::new(MD065BlanksAroundHorizontalRules)
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::lint_context::LintContext;
245
246 #[test]
247 fn test_hr_with_blanks() {
248 let rule = MD065BlanksAroundHorizontalRules;
249 let content = "Some text before.
250
251---
252
253Some text after.";
254 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
255 let result = rule.check(&ctx).unwrap();
256
257 assert!(result.is_empty());
258 }
259
260 #[test]
261 fn test_hr_missing_blank_before() {
262 let rule = MD065BlanksAroundHorizontalRules;
263 let content = "Some text before.
265***
266
267Some text after.";
268 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
269 let result = rule.check(&ctx).unwrap();
270
271 assert_eq!(result.len(), 1);
272 assert_eq!(result[0].line, 2);
273 assert!(result[0].message.contains("before horizontal rule"));
274 }
275
276 #[test]
277 fn test_hr_missing_blank_after() {
278 let rule = MD065BlanksAroundHorizontalRules;
279 let content = "Some text before.
280
281***
282Some text after.";
283 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
284 let result = rule.check(&ctx).unwrap();
285
286 assert_eq!(result.len(), 1);
287 assert_eq!(result[0].line, 3);
288 assert!(result[0].message.contains("after horizontal rule"));
289 }
290
291 #[test]
292 fn test_hr_missing_both_blanks() {
293 let rule = MD065BlanksAroundHorizontalRules;
294 let content = "Some text before.
296***
297Some text after.";
298 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
299 let result = rule.check(&ctx).unwrap();
300
301 assert_eq!(result.len(), 2);
302 assert!(result[0].message.contains("before horizontal rule"));
303 assert!(result[1].message.contains("after horizontal rule"));
304 }
305
306 #[test]
307 fn test_hr_at_start_of_document() {
308 let rule = MD065BlanksAroundHorizontalRules;
309 let content = "---
310
311Some text after.";
312 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
313 let result = rule.check(&ctx).unwrap();
314
315 assert!(result.is_empty());
317 }
318
319 #[test]
320 fn test_hr_at_end_of_document() {
321 let rule = MD065BlanksAroundHorizontalRules;
322 let content = "Some text before.
323
324---";
325 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
326 let result = rule.check(&ctx).unwrap();
327
328 assert!(result.is_empty());
330 }
331
332 #[test]
333 fn test_multiple_hrs() {
334 let rule = MD065BlanksAroundHorizontalRules;
335 let content = "Text before.
337***
338Middle text.
339___
340Text after.";
341 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
342 let result = rule.check(&ctx).unwrap();
343
344 assert_eq!(result.len(), 4);
345 }
346
347 #[test]
348 fn test_hr_asterisks() {
349 let rule = MD065BlanksAroundHorizontalRules;
350 let content = "Some text.
351***
352More text.";
353 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
354 let result = rule.check(&ctx).unwrap();
355
356 assert_eq!(result.len(), 2);
357 }
358
359 #[test]
360 fn test_hr_underscores() {
361 let rule = MD065BlanksAroundHorizontalRules;
362 let content = "Some text.
363___
364More text.";
365 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
366 let result = rule.check(&ctx).unwrap();
367
368 assert_eq!(result.len(), 2);
369 }
370
371 #[test]
372 fn test_hr_with_spaces() {
373 let rule = MD065BlanksAroundHorizontalRules;
374 let content = "Some text.
376* * *
377More text.";
378 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
379 let result = rule.check(&ctx).unwrap();
380
381 assert_eq!(result.len(), 2);
382 }
383
384 #[test]
385 fn test_hr_long() {
386 let rule = MD065BlanksAroundHorizontalRules;
387 let content = "Some text.
389**********
390More text.";
391 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
392 let result = rule.check(&ctx).unwrap();
393
394 assert_eq!(result.len(), 2);
395 }
396
397 #[test]
398 fn test_setext_heading_not_hr() {
399 let rule = MD065BlanksAroundHorizontalRules;
400 let content = "Heading
401---
402
403More text.";
404 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
405 let result = rule.check(&ctx).unwrap();
406
407 assert!(result.is_empty());
409 }
410
411 #[test]
412 fn test_setext_heading_equals() {
413 let rule = MD065BlanksAroundHorizontalRules;
414 let content = "Heading
415===
416
417More text.";
418 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
419 let result = rule.check(&ctx).unwrap();
420
421 assert!(result.is_empty());
423 }
424
425 #[test]
426 fn test_hr_in_code_block() {
427 let rule = MD065BlanksAroundHorizontalRules;
428 let content = "Some text.
429
430```
431---
432```
433
434More text.";
435 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
436 let result = rule.check(&ctx).unwrap();
437
438 assert!(result.is_empty());
440 }
441
442 #[test]
443 fn test_fix_missing_blanks() {
444 let rule = MD065BlanksAroundHorizontalRules;
445 let content = "Text before.
447***
448Text after.";
449 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
450 let fixed = rule.fix(&ctx).unwrap();
451
452 let expected = "Text before.
453
454***
455
456Text after.";
457 assert_eq!(fixed, expected);
458 }
459
460 #[test]
461 fn test_fix_multiple_hrs() {
462 let rule = MD065BlanksAroundHorizontalRules;
463 let content = "Start
465***
466Middle
467___
468End";
469 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
470 let fixed = rule.fix(&ctx).unwrap();
471
472 let expected = "Start
473
474***
475
476Middle
477
478___
479
480End";
481 assert_eq!(fixed, expected);
482 }
483
484 #[test]
485 fn test_empty_content() {
486 let rule = MD065BlanksAroundHorizontalRules;
487 let content = "";
488 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
489 let result = rule.check(&ctx).unwrap();
490
491 assert!(result.is_empty());
492 }
493
494 #[test]
495 fn test_no_hrs() {
496 let rule = MD065BlanksAroundHorizontalRules;
497 let content = "Just regular text.
498No horizontal rules here.
499Only paragraphs.";
500 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
501 let result = rule.check(&ctx).unwrap();
502
503 assert!(result.is_empty());
504 }
505
506 #[test]
507 fn test_is_horizontal_rule() {
508 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("---"));
510 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("----"));
511 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("***"));
512 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("****"));
513 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("___"));
514 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("____"));
515 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("- - -"));
516 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("* * *"));
517 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("_ _ _"));
518 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule(" --- "));
519
520 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule("--"));
522 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule("**"));
523 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule("__"));
524 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule("- -"));
525 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule("text"));
526 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule(""));
527 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule("==="));
528 }
529
530 #[test]
531 fn test_consecutive_hrs_with_blanks() {
532 let rule = MD065BlanksAroundHorizontalRules;
533 let content = "Text.
534
535---
536
537***
538
539More text.";
540 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
541 let result = rule.check(&ctx).unwrap();
542
543 assert!(result.is_empty());
545 }
546
547 #[test]
548 fn test_hr_after_heading() {
549 let rule = MD065BlanksAroundHorizontalRules;
550 let content = "# Heading
552***
553
554Text.";
555 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
556 let result = rule.check(&ctx).unwrap();
557
558 assert_eq!(result.len(), 1);
560 assert!(result[0].message.contains("before horizontal rule"));
561 }
562
563 #[test]
564 fn test_hr_before_heading() {
565 let rule = MD065BlanksAroundHorizontalRules;
566 let content = "Text.
567
568***
569# Heading";
570 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
571 let result = rule.check(&ctx).unwrap();
572
573 assert_eq!(result.len(), 1);
575 assert!(result[0].message.contains("after horizontal rule"));
576 }
577
578 #[test]
579 fn test_setext_heading_hyphen_not_flagged() {
580 let rule = MD065BlanksAroundHorizontalRules;
581 let content = "Heading Text
583---
584
585More text.";
586 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
587 let result = rule.check(&ctx).unwrap();
588
589 assert!(result.is_empty());
591 }
592
593 #[test]
594 fn test_hr_with_blank_before_hyphen() {
595 let rule = MD065BlanksAroundHorizontalRules;
596 let content = "Some text.
598
599---
600More text.";
601 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
602 let result = rule.check(&ctx).unwrap();
603
604 assert_eq!(result.len(), 1);
606 assert!(result[0].message.contains("after horizontal rule"));
607 }
608
609 #[test]
614 fn test_frontmatter_not_flagged() {
615 let rule = MD065BlanksAroundHorizontalRules;
616 let content = "---
618title: Test Document
619date: 2024-01-01
620---
621
622# Heading
623
624Content here.";
625 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
626 let result = rule.check(&ctx).unwrap();
627
628 assert!(result.is_empty());
630 }
631
632 #[test]
633 fn test_hr_after_frontmatter() {
634 let rule = MD065BlanksAroundHorizontalRules;
635 let content = "---
636title: Test
637---
638
639Content.
640***
641More content.";
642 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
643 let result = rule.check(&ctx).unwrap();
644
645 assert_eq!(result.len(), 2);
647 }
648
649 #[test]
650 fn test_hr_in_indented_code_block() {
651 let rule = MD065BlanksAroundHorizontalRules;
652 let content = "Some text.
654
655 ---
656 code here
657
658More text.";
659 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
660 let result = rule.check(&ctx).unwrap();
661
662 assert!(result.is_empty());
664 }
665
666 #[test]
667 fn test_hr_with_leading_spaces() {
668 let rule = MD065BlanksAroundHorizontalRules;
669 let content = "Text.
671 ***
672More text.";
673 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
674 let result = rule.check(&ctx).unwrap();
675
676 assert_eq!(result.len(), 2);
678 }
679
680 #[test]
681 fn test_hr_in_html_comment() {
682 let rule = MD065BlanksAroundHorizontalRules;
683 let content = "Text.
684
685<!--
686---
687-->
688
689More text.";
690 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
691 let result = rule.check(&ctx).unwrap();
692
693 assert!(result.is_empty());
695 }
696
697 #[test]
698 fn test_hr_in_blockquote() {
699 let rule = MD065BlanksAroundHorizontalRules;
700 let content = "Text.
701
702> Quote text
703> ***
704> More quote
705
706After quote.";
707 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
708 let result = rule.check(&ctx).unwrap();
709
710 assert!(result.len() <= 2); }
716
717 #[test]
718 fn test_hr_after_list() {
719 let rule = MD065BlanksAroundHorizontalRules;
720 let content = "* Item one
722* Item two
723***
724
725More text.";
726 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
727 let result = rule.check(&ctx).unwrap();
728
729 assert_eq!(result.len(), 1);
731 assert!(result[0].message.contains("before horizontal rule"));
732 }
733
734 #[test]
735 fn test_mixed_marker_with_many_spaces() {
736 let rule = MD065BlanksAroundHorizontalRules;
737 let content = "Text.
738- - - -
739More text.";
740 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
741 let result = rule.check(&ctx).unwrap();
742
743 assert_eq!(result.len(), 2);
745 }
746
747 #[test]
748 fn test_only_hr_in_document() {
749 let rule = MD065BlanksAroundHorizontalRules;
750 let content = "---";
751 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
752 let result = rule.check(&ctx).unwrap();
753
754 assert!(result.is_empty());
756 }
757
758 #[test]
759 fn test_multiple_blank_lines_already_present() {
760 let rule = MD065BlanksAroundHorizontalRules;
761 let content = "Text.
762
763
764---
765
766
767More text.";
768 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
769 let result = rule.check(&ctx).unwrap();
770
771 assert!(result.is_empty());
773 }
774
775 #[test]
776 fn test_hr_at_both_start_and_end() {
777 let rule = MD065BlanksAroundHorizontalRules;
778 let content = "---
779
780Content in the middle.
781
782---";
783 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
784 let result = rule.check(&ctx).unwrap();
785
786 assert!(result.is_empty());
788 }
789
790 #[test]
791 fn test_consecutive_hrs_without_blanks() {
792 let rule = MD065BlanksAroundHorizontalRules;
793 let content = "Text.
794
795***
796---
797___
798
799More text.";
800 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
801 let result = rule.check(&ctx).unwrap();
802
803 assert!(result.len() >= 2);
808 }
809
810 #[test]
811 fn test_fix_idempotency() {
812 let rule = MD065BlanksAroundHorizontalRules;
813 let content = "Text before.
814***
815Text after.";
816 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
817 let fixed_once = rule.fix(&ctx).unwrap();
818
819 let ctx2 = LintContext::new(&fixed_once, crate::config::MarkdownFlavor::Standard, None);
821 let fixed_twice = rule.fix(&ctx2).unwrap();
822
823 assert_eq!(fixed_once, fixed_twice);
825 }
826
827 #[test]
828 fn test_setext_heading_long_underline() {
829 let rule = MD065BlanksAroundHorizontalRules;
830 let content = "Heading Text
831----------
832
833More text.";
834 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
835 let result = rule.check(&ctx).unwrap();
836
837 assert!(result.is_empty());
839 }
840
841 #[test]
842 fn test_hr_with_trailing_whitespace() {
843 let rule = MD065BlanksAroundHorizontalRules;
844 let content = "Text.
845***
846More text.";
847 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
848 let result = rule.check(&ctx).unwrap();
849
850 assert_eq!(result.len(), 2);
852 }
853
854 #[test]
855 fn test_hr_in_html_block() {
856 let rule = MD065BlanksAroundHorizontalRules;
857 let content = "Text.
858
859<div>
860---
861</div>
862
863More text.";
864 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
865 let result = rule.check(&ctx).unwrap();
866
867 assert!(result.is_empty());
870 }
871
872 #[test]
873 fn test_spaced_hyphens_are_hr_not_setext() {
874 let rule = MD065BlanksAroundHorizontalRules;
875 let content = "Heading
878- - -
879
880More text.";
881 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
882 let result = rule.check(&ctx).unwrap();
883
884 assert_eq!(result.len(), 1);
886 assert!(result[0].message.contains("before horizontal rule"));
887 }
888
889 #[test]
890 fn test_not_setext_if_prev_line_blank() {
891 let rule = MD065BlanksAroundHorizontalRules;
892 let content = "Some paragraph.
893
894---
895Text after.";
896 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
897 let result = rule.check(&ctx).unwrap();
898
899 assert_eq!(result.len(), 1);
901 assert!(result[0].message.contains("after horizontal rule"));
902 }
903
904 #[test]
905 fn test_asterisk_cannot_be_setext() {
906 let rule = MD065BlanksAroundHorizontalRules;
907 let content = "Some text
909***
910More text.";
911 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
912 let result = rule.check(&ctx).unwrap();
913
914 assert_eq!(result.len(), 2);
916 }
917
918 #[test]
919 fn test_underscore_cannot_be_setext() {
920 let rule = MD065BlanksAroundHorizontalRules;
921 let content = "Some text
923___
924More text.";
925 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
926 let result = rule.check(&ctx).unwrap();
927
928 assert_eq!(result.len(), 2);
930 }
931
932 #[test]
933 fn test_fix_preserves_content() {
934 let rule = MD065BlanksAroundHorizontalRules;
935 let content = "First paragraph with **bold** and *italic*.
936***
937Second paragraph with [link](url) and `code`.";
938 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
939 let fixed = rule.fix(&ctx).unwrap();
940
941 assert!(fixed.contains("**bold**"));
943 assert!(fixed.contains("*italic*"));
944 assert!(fixed.contains("[link](url)"));
945 assert!(fixed.contains("`code`"));
946 assert!(fixed.contains("***"));
947 }
948
949 #[test]
950 fn test_fix_only_adds_needed_blanks() {
951 let rule = MD065BlanksAroundHorizontalRules;
952 let content = "Text.
954
955***
956More text.";
957 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
958 let fixed = rule.fix(&ctx).unwrap();
959
960 let expected = "Text.
961
962***
963
964More text.";
965 assert_eq!(fixed, expected);
966 }
967
968 #[test]
969 fn test_hr_detection_edge_cases() {
970 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule(" ---"));
974 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("--- "));
975 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule(" --- "));
976 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("* * *"));
977 assert!(MD065BlanksAroundHorizontalRules::is_horizontal_rule("_ _ _"));
978
979 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule("--a"));
981 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule("**a"));
982 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule("-*-"));
983 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule("- * _"));
984 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule(" "));
985 assert!(!MD065BlanksAroundHorizontalRules::is_horizontal_rule("\t---"));
986 }
987
988 #[test]
989 fn test_warning_line_numbers_accurate() {
990 let rule = MD065BlanksAroundHorizontalRules;
991 let content = "Line 1
992Line 2
993***
994Line 4";
995 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
996 let result = rule.check(&ctx).unwrap();
997
998 assert_eq!(result.len(), 2);
1000 assert_eq!(result[0].line, 3); assert_eq!(result[1].line, 3);
1002 }
1003
1004 #[test]
1005 fn test_complex_document_structure() {
1006 let rule = MD065BlanksAroundHorizontalRules;
1007 let content = "# Main Title
1008
1009Introduction paragraph.
1010
1011## Section One
1012
1013Content here.
1014
1015***
1016
1017## Section Two
1018
1019More content.
1020
1021---
1022
1023Final thoughts.";
1024 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1025 let result = rule.check(&ctx).unwrap();
1026
1027 assert!(result.is_empty());
1029 }
1030}