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