1use crate::utils::range_utils::calculate_line_range;
7
8use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
9use toml;
10
11mod md035_config;
12use md035_config::MD035Config;
13
14#[derive(Clone, Default)]
16pub struct MD035HRStyle {
17 config: MD035Config,
18}
19
20impl MD035HRStyle {
21 pub fn new(style: String) -> Self {
22 Self {
23 config: MD035Config { style },
24 }
25 }
26
27 pub fn from_config_struct(config: MD035Config) -> Self {
28 Self { config }
29 }
30
31 fn is_horizontal_rule(line: &str) -> bool {
32 crate::utils::thematic_break::is_thematic_break(line)
33 }
34
35 fn is_potential_setext_heading(lines: &[&str], i: usize) -> bool {
37 if i == 0 {
38 return false; }
40
41 let line = lines[i].trim();
42 let prev_line = lines[i - 1].trim();
43
44 let is_dash_line = !line.is_empty() && line.chars().all(|c| c == '-');
45 let is_equals_line = !line.is_empty() && line.chars().all(|c| c == '=');
46 let prev_line_has_content = !prev_line.is_empty() && !Self::is_horizontal_rule(prev_line);
47 (is_dash_line || is_equals_line) && prev_line_has_content
48 }
49
50 fn most_prevalent_hr_style(lines: &[&str], ctx: &crate::lint_context::LintContext) -> Option<String> {
52 use std::collections::HashMap;
53 let mut counts: HashMap<&str, usize> = HashMap::new();
54 let mut order: Vec<&str> = Vec::new();
55 for (i, line) in lines.iter().enumerate() {
56 if let Some(line_info) = ctx.lines.get(i)
58 && (line_info.in_front_matter || line_info.in_code_block || line_info.in_mkdocs_html_markdown)
59 {
60 continue;
61 }
62
63 if Self::is_horizontal_rule(line) && !Self::is_potential_setext_heading(lines, i) {
64 let style = line.trim();
65 let counter = counts.entry(style).or_insert(0);
66 *counter += 1;
67 if *counter == 1 {
68 order.push(style);
69 }
70 }
71 }
72 counts
74 .iter()
75 .max_by_key(|&(style, count)| {
76 (
77 *count,
78 -(order.iter().position(|&s| s == *style).unwrap_or(usize::MAX) as isize),
79 )
80 })
81 .map(|(style, _)| style.to_string())
82 }
83}
84
85impl Rule for MD035HRStyle {
86 fn name(&self) -> &'static str {
87 "MD035"
88 }
89
90 fn description(&self) -> &'static str {
91 "Horizontal rule style"
92 }
93
94 fn category(&self) -> RuleCategory {
95 RuleCategory::Whitespace
96 }
97
98 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
99 let line_index = &ctx.line_index;
100
101 let mut warnings = Vec::new();
102 let lines = ctx.raw_lines();
103
104 let expected_style = if self.config.style.is_empty() || self.config.style == "consistent" {
106 Self::most_prevalent_hr_style(lines, ctx).unwrap_or_else(|| "---".to_string())
107 } else {
108 self.config.style.clone()
109 };
110
111 for (i, line) in lines.iter().enumerate() {
112 if let Some(line_info) = ctx.lines.get(i)
114 && (line_info.in_front_matter || line_info.in_code_block || line_info.in_mkdocs_html_markdown)
115 {
116 continue;
117 }
118
119 if Self::is_potential_setext_heading(lines, i) {
121 continue;
122 }
123
124 if Self::is_horizontal_rule(line) {
125 let has_indentation = line.len() > line.trim_start().len();
127 let style_mismatch = line.trim() != expected_style;
128
129 if style_mismatch || has_indentation {
130 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
132
133 warnings.push(LintWarning {
134 rule_name: Some(self.name().to_string()),
135 line: start_line,
136 column: start_col,
137 end_line,
138 end_column: end_col,
139 message: if has_indentation {
140 "Horizontal rule should not be indented".to_string()
141 } else {
142 format!("Horizontal rule style should be \"{expected_style}\"")
143 },
144 severity: Severity::Warning,
145 fix: Some(Fix::new(
146 line_index.line_col_to_byte_range_with_length(i + 1, 1, line.chars().count()),
147 expected_style.clone(),
148 )),
149 });
150 }
151 }
152 }
153
154 Ok(warnings)
155 }
156
157 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
158 if self.should_skip(ctx) {
159 return Ok(ctx.content.to_string());
160 }
161 let warnings = self.check(ctx)?;
162 if warnings.is_empty() {
163 return Ok(ctx.content.to_string());
164 }
165 let warnings =
166 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
167 crate::utils::fix_utils::apply_warning_fixes(ctx.content, &warnings)
168 .map_err(crate::rule::LintError::InvalidInput)
169 }
170
171 fn as_any(&self) -> &dyn std::any::Any {
172 self
173 }
174
175 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
177 ctx.content.is_empty() || (!ctx.has_char('-') && !ctx.has_char('*') && !ctx.has_char('_'))
179 }
180
181 crate::impl_rule_config_methods!(MD035Config);
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::lint_context::LintContext;
188
189 #[test]
190 fn test_is_horizontal_rule() {
191 assert!(MD035HRStyle::is_horizontal_rule("---"));
193 assert!(MD035HRStyle::is_horizontal_rule("----"));
194 assert!(MD035HRStyle::is_horizontal_rule("***"));
195 assert!(MD035HRStyle::is_horizontal_rule("****"));
196 assert!(MD035HRStyle::is_horizontal_rule("___"));
197 assert!(MD035HRStyle::is_horizontal_rule("____"));
198 assert!(MD035HRStyle::is_horizontal_rule("- - -"));
199 assert!(MD035HRStyle::is_horizontal_rule("* * *"));
200 assert!(MD035HRStyle::is_horizontal_rule("_ _ _"));
201 assert!(MD035HRStyle::is_horizontal_rule(" --- ")); assert!(!MD035HRStyle::is_horizontal_rule("--")); assert!(!MD035HRStyle::is_horizontal_rule("**"));
206 assert!(!MD035HRStyle::is_horizontal_rule("__"));
207 assert!(!MD035HRStyle::is_horizontal_rule("- -")); assert!(!MD035HRStyle::is_horizontal_rule("* *"));
209 assert!(!MD035HRStyle::is_horizontal_rule("_ _"));
210 assert!(!MD035HRStyle::is_horizontal_rule("text"));
211 assert!(!MD035HRStyle::is_horizontal_rule(""));
212 }
213
214 #[test]
215 fn test_is_potential_setext_heading() {
216 let lines = vec!["Heading 1", "=========", "Content", "Heading 2", "---", "More content"];
217
218 assert!(MD035HRStyle::is_potential_setext_heading(&lines, 1)); assert!(MD035HRStyle::is_potential_setext_heading(&lines, 4)); assert!(!MD035HRStyle::is_potential_setext_heading(&lines, 0)); assert!(!MD035HRStyle::is_potential_setext_heading(&lines, 2)); let lines2 = vec!["", "---", "Content"];
227 assert!(!MD035HRStyle::is_potential_setext_heading(&lines2, 1)); let lines3 = vec!["***", "---"];
230 assert!(!MD035HRStyle::is_potential_setext_heading(&lines3, 1)); }
232
233 #[test]
234 fn test_most_prevalent_hr_style() {
235 let content = "Content\n\n---\n\nMore\n\n---\n\nText";
237 let lines: Vec<&str> = content.lines().collect();
238 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
239 assert_eq!(
240 MD035HRStyle::most_prevalent_hr_style(&lines, &ctx),
241 Some("---".to_string())
242 );
243
244 let content = "Content\n\n---\n\nMore\n\n***\n\nText\n\n---";
246 let lines: Vec<&str> = content.lines().collect();
247 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
248 assert_eq!(
249 MD035HRStyle::most_prevalent_hr_style(&lines, &ctx),
250 Some("---".to_string())
251 );
252
253 let content = "Content\n\n***\n\nMore\n\n---\n\nText";
255 let lines: Vec<&str> = content.lines().collect();
256 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
257 assert_eq!(
258 MD035HRStyle::most_prevalent_hr_style(&lines, &ctx),
259 Some("***".to_string())
260 );
261
262 let content = "Just\nRegular\nContent";
264 let lines: Vec<&str> = content.lines().collect();
265 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
266 assert_eq!(MD035HRStyle::most_prevalent_hr_style(&lines, &ctx), None);
267
268 let content = "Heading\n---\nContent\n\n***";
270 let lines: Vec<&str> = content.lines().collect();
271 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
272 assert_eq!(
273 MD035HRStyle::most_prevalent_hr_style(&lines, &ctx),
274 Some("***".to_string())
275 );
276 }
277
278 #[test]
279 fn test_consistent_style() {
280 let rule = MD035HRStyle::new("consistent".to_string());
281 let content = "Content\n\n---\n\nMore\n\n***\n\nText\n\n---";
282 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
283 let result = rule.check(&ctx).unwrap();
284
285 assert_eq!(result.len(), 1);
287 assert_eq!(result[0].line, 7);
288 assert!(result[0].message.contains("Horizontal rule style should be \"---\""));
289 }
290
291 #[test]
292 fn test_specific_style_dashes() {
293 let rule = MD035HRStyle::new("---".to_string());
294 let content = "Content\n\n***\n\nMore\n\n___\n\nText";
295 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
296 let result = rule.check(&ctx).unwrap();
297
298 assert_eq!(result.len(), 2);
300 assert_eq!(result[0].line, 3);
301 assert_eq!(result[1].line, 7);
302 assert!(result[0].message.contains("Horizontal rule style should be \"---\""));
303 }
304
305 #[test]
306 fn test_indented_horizontal_rule() {
307 let rule = MD035HRStyle::new("---".to_string());
308 let content = "Content\n\n ---\n\nMore";
309 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
310 let result = rule.check(&ctx).unwrap();
311
312 assert_eq!(result.len(), 1);
313 assert_eq!(result[0].line, 3);
314 assert_eq!(result[0].message, "Horizontal rule should not be indented");
315 }
316
317 #[test]
318 fn test_setext_heading_not_flagged() {
319 let rule = MD035HRStyle::new("***".to_string());
320 let content = "Heading\n---\nContent\n***";
321 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
322 let result = rule.check(&ctx).unwrap();
323
324 assert_eq!(result.len(), 0);
326 }
327
328 #[test]
329 fn test_fix_consistent_style() {
330 let rule = MD035HRStyle::new("consistent".to_string());
331 let content = "Content\n\n---\n\nMore\n\n***\n\nText\n\n---";
332 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
333 let fixed = rule.fix(&ctx).unwrap();
334
335 let expected = "Content\n\n---\n\nMore\n\n---\n\nText\n\n---";
336 assert_eq!(fixed, expected);
337 }
338
339 #[test]
340 fn test_fix_specific_style() {
341 let rule = MD035HRStyle::new("***".to_string());
342 let content = "Content\n\n---\n\nMore\n\n___\n\nText";
343 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
344 let fixed = rule.fix(&ctx).unwrap();
345
346 let expected = "Content\n\n***\n\nMore\n\n***\n\nText";
347 assert_eq!(fixed, expected);
348 }
349
350 #[test]
351 fn test_fix_preserves_setext_headings() {
352 let rule = MD035HRStyle::new("***".to_string());
353 let content = "Heading 1\n=========\nHeading 2\n---\nContent\n\n---";
354 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
355 let fixed = rule.fix(&ctx).unwrap();
356
357 let expected = "Heading 1\n=========\nHeading 2\n---\nContent\n\n***";
358 assert_eq!(fixed, expected);
359 }
360
361 #[test]
362 fn test_fix_removes_indentation() {
363 let rule = MD035HRStyle::new("---".to_string());
364 let content = "Content\n\n ***\n\nMore\n\n ___\n\nText";
365 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
366 let fixed = rule.fix(&ctx).unwrap();
367
368 let expected = "Content\n\n---\n\nMore\n\n---\n\nText";
369 assert_eq!(fixed, expected);
370 }
371
372 #[test]
373 fn test_spaced_styles() {
374 let rule = MD035HRStyle::new("* * *".to_string());
375 let content = "Content\n\n- - -\n\nMore\n\n_ _ _\n\nText";
376 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
377 let result = rule.check(&ctx).unwrap();
378
379 assert_eq!(result.len(), 2);
380 assert!(result[0].message.contains("Horizontal rule style should be \"* * *\""));
381 }
382
383 #[test]
384 fn test_empty_style_uses_consistent() {
385 let rule = MD035HRStyle::new("".to_string());
386 let content = "Content\n\n---\n\nMore\n\n***\n\nText";
387 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
388 let result = rule.check(&ctx).unwrap();
389
390 assert_eq!(result.len(), 1);
392 assert_eq!(result[0].line, 7);
393 }
394
395 #[test]
396 fn test_all_hr_styles_consistent() {
397 let rule = MD035HRStyle::new("consistent".to_string());
398 let content = "Content\n---\nMore\n---\nText\n---";
399 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
400 let result = rule.check(&ctx).unwrap();
401
402 assert_eq!(result.len(), 0);
404 }
405
406 #[test]
407 fn test_no_horizontal_rules() {
408 let rule = MD035HRStyle::new("---".to_string());
409 let content = "Just regular content\nNo horizontal rules here";
410 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
411 let result = rule.check(&ctx).unwrap();
412
413 assert_eq!(result.len(), 0);
414 }
415
416 #[test]
417 fn test_mixed_spaced_and_unspaced() {
418 let rule = MD035HRStyle::new("consistent".to_string());
419 let content = "Content\n\n---\n\nMore\n\n- - -\n\nText";
420 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
421 let result = rule.check(&ctx).unwrap();
422
423 assert_eq!(result.len(), 1);
425 assert_eq!(result[0].line, 7);
426 }
427
428 #[test]
429 fn test_trailing_whitespace_in_hr() {
430 let rule = MD035HRStyle::new("---".to_string());
431 let content = "Content\n\n--- \n\nMore";
432 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
433 let result = rule.check(&ctx).unwrap();
434
435 assert_eq!(result.len(), 0);
437 }
438
439 #[test]
440 fn test_hr_in_code_block_not_flagged() {
441 let rule = MD035HRStyle::new("---".to_string());
442 let content =
443 "Text\n\n```bash\n----------------------------------------------------------------------\n```\n\nMore";
444 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
445 let result = rule.check(&ctx).unwrap();
446
447 assert_eq!(result.len(), 0);
449 }
450
451 #[test]
452 fn test_hr_in_code_span_not_flagged() {
453 let rule = MD035HRStyle::new("---".to_string());
454 let content = "Text with inline `---` code span";
455 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
456 let result = rule.check(&ctx).unwrap();
457
458 assert_eq!(result.len(), 0);
460 }
461
462 #[test]
463 fn test_hr_with_extra_characters() {
464 let rule = MD035HRStyle::new("---".to_string());
465 let content = "Content\n-----\nMore\n--------\nText";
466 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
467 let result = rule.check(&ctx).unwrap();
468
469 assert_eq!(result.len(), 0);
471 }
472
473 #[test]
474 fn test_default_config() {
475 let style = |rule: &MD035HRStyle| {
479 let (name, config) = rule.default_config_section().unwrap();
480 assert_eq!(name, "MD035");
481 config
482 .as_table()
483 .unwrap()
484 .get("style")
485 .unwrap()
486 .as_str()
487 .unwrap()
488 .to_string()
489 };
490
491 assert_eq!(style(&MD035HRStyle::default()), "consistent");
494 assert_eq!(
495 style(&MD035HRStyle::new("***".to_string())),
496 "consistent",
497 "a configured instance must still publish the default"
498 );
499 }
500
501 #[test]
502 fn test_fix_skips_mkdocs_html_markdown() {
503 let rule = MD035HRStyle::new("***".to_string());
506
507 let content = "Some content\n\n***\n\n<div class=\"grid cards\" markdown>\n\n- Card 1 content\n\n ---\n\n Card 1 footer\n\n</div>\n";
508 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
509
510 let warnings = rule.check(&ctx).unwrap();
512 for w in &warnings {
513 assert_ne!(w.line, 9, "check() should not flag --- inside <div markdown> block");
514 }
515
516 let fixed = rule.fix(&ctx).unwrap();
518 assert!(
519 fixed.contains(" ---"),
520 "fix() should preserve --- inside <div markdown> block, got: {fixed}"
521 );
522 }
523
524 #[test]
525 fn test_is_horizontal_rule_edge_cases() {
526 assert!(MD035HRStyle::is_horizontal_rule("----------"));
528 assert!(MD035HRStyle::is_horizontal_rule("**********"));
529 assert!(MD035HRStyle::is_horizontal_rule("__________"));
530
531 assert!(MD035HRStyle::is_horizontal_rule("- - - -"));
533 assert!(MD035HRStyle::is_horizontal_rule("* * * * *"));
534 assert!(MD035HRStyle::is_horizontal_rule("_ _ _ _ _ _"));
535
536 assert!(MD035HRStyle::is_horizontal_rule("* * *"));
538 assert!(MD035HRStyle::is_horizontal_rule("- - -"));
539 assert!(MD035HRStyle::is_horizontal_rule("_ _ _"));
540
541 assert!(MD035HRStyle::is_horizontal_rule("--- "));
543 assert!(MD035HRStyle::is_horizontal_rule("*** "));
544 assert!(MD035HRStyle::is_horizontal_rule("___ "));
545
546 assert!(MD035HRStyle::is_horizontal_rule("- - - "));
548 assert!(MD035HRStyle::is_horizontal_rule("* * * "));
549
550 assert!(!MD035HRStyle::is_horizontal_rule("-*-"));
552 assert!(!MD035HRStyle::is_horizontal_rule("- * -"));
553 assert!(!MD035HRStyle::is_horizontal_rule("_-_"));
554 assert!(!MD035HRStyle::is_horizontal_rule("*_*"));
555
556 assert!(!MD035HRStyle::is_horizontal_rule("---text"));
558 assert!(!MD035HRStyle::is_horizontal_rule("***text"));
559 assert!(!MD035HRStyle::is_horizontal_rule("- - - text"));
560
561 assert!(!MD035HRStyle::is_horizontal_rule("- -"));
563 assert!(!MD035HRStyle::is_horizontal_rule("* *"));
564 assert!(!MD035HRStyle::is_horizontal_rule("_ _"));
565
566 assert!(!MD035HRStyle::is_horizontal_rule("-a-b-"));
568 assert!(!MD035HRStyle::is_horizontal_rule("*x*x*"));
569
570 assert!(!MD035HRStyle::is_horizontal_rule("-"));
572 assert!(!MD035HRStyle::is_horizontal_rule("*"));
573 assert!(!MD035HRStyle::is_horizontal_rule("_"));
574
575 assert!(MD035HRStyle::is_horizontal_rule("*\t*\t*"));
577 assert!(MD035HRStyle::is_horizontal_rule("-\t-\t-"));
578
579 let long_hr = "-".repeat(200);
581 assert!(MD035HRStyle::is_horizontal_rule(&long_hr));
582 }
583
584 #[test]
585 fn test_frontmatter_not_treated_as_hr() {
586 let rule = MD035HRStyle::new("***".to_string());
587 let content = "---\ntitle: Test\n---\n\n***\n\nContent";
588 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
589 let result = rule.check(&ctx).unwrap();
590
591 assert_eq!(result.len(), 0);
593 }
594
595 #[test]
596 fn test_fix_skips_mkdocs_html_markdown_preserves_outside() {
597 let rule = MD035HRStyle::new("***".to_string());
599
600 let content = "Some content\n\n---\n\n<div class=\"grid cards\" markdown>\n\n- Card content\n\n ---\n\n Card footer\n\n</div>\n";
601 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
602
603 let fixed = rule.fix(&ctx).unwrap();
604 let lines: Vec<&str> = fixed.lines().collect();
606 assert_eq!(lines[2], "***", "fix() should change --- outside <div markdown> to ***");
607 assert!(
609 fixed.contains(" ---"),
610 "fix() should preserve --- inside <div markdown>"
611 );
612 }
613
614 fn assert_fix_roundtrip(rule: &MD035HRStyle, content: &str, flavor: crate::config::MarkdownFlavor) {
616 let ctx = LintContext::new(content, flavor, None);
617 let fixed = rule.fix(&ctx).unwrap();
618 let ctx2 = LintContext::new(&fixed, flavor, None);
619 let warnings = rule.check(&ctx2).unwrap();
620 assert!(
621 warnings.is_empty(),
622 "fix() output should produce zero check() warnings.\nOriginal:\n{content}\nFixed:\n{fixed}\nWarnings: {warnings:?}"
623 );
624 }
625
626 #[test]
627 fn test_roundtrip_consistent_style() {
628 let rule = MD035HRStyle::new("consistent".to_string());
629 assert_fix_roundtrip(
630 &rule,
631 "Content\n\n---\n\nMore\n\n***\n\nText\n\n---",
632 crate::config::MarkdownFlavor::Standard,
633 );
634 }
635
636 #[test]
637 fn test_roundtrip_specific_style() {
638 let rule = MD035HRStyle::new("***".to_string());
639 assert_fix_roundtrip(
640 &rule,
641 "Content\n\n---\n\nMore\n\n___\n\nText",
642 crate::config::MarkdownFlavor::Standard,
643 );
644 }
645
646 #[test]
647 fn test_roundtrip_indented_hr() {
648 let rule = MD035HRStyle::new("---".to_string());
649 assert_fix_roundtrip(
650 &rule,
651 "Content\n\n ***\n\nMore\n\n ___\n\nText",
652 crate::config::MarkdownFlavor::Standard,
653 );
654 }
655
656 #[test]
657 fn test_roundtrip_setext_headings() {
658 let rule = MD035HRStyle::new("***".to_string());
659 assert_fix_roundtrip(
660 &rule,
661 "Heading 1\n=========\nHeading 2\n---\nContent\n\n---",
662 crate::config::MarkdownFlavor::Standard,
663 );
664 }
665
666 #[test]
667 fn test_roundtrip_frontmatter() {
668 let rule = MD035HRStyle::new("***".to_string());
669 assert_fix_roundtrip(
670 &rule,
671 "---\ntitle: Test\n---\n\n***\n\nContent",
672 crate::config::MarkdownFlavor::Standard,
673 );
674 }
675
676 #[test]
677 fn test_roundtrip_mkdocs_html_markdown() {
678 let rule = MD035HRStyle::new("***".to_string());
679 let content = "Some content\n\n---\n\n<div class=\"grid cards\" markdown>\n\n- Card content\n\n ---\n\n Card footer\n\n</div>\n";
680 assert_fix_roundtrip(&rule, content, crate::config::MarkdownFlavor::MkDocs);
681 }
682
683 #[test]
684 fn test_roundtrip_spaced_styles() {
685 let rule = MD035HRStyle::new("* * *".to_string());
686 assert_fix_roundtrip(
687 &rule,
688 "Content\n\n- - -\n\nMore\n\n_ _ _\n\nText",
689 crate::config::MarkdownFlavor::Standard,
690 );
691 }
692
693 #[test]
694 fn test_roundtrip_no_warnings() {
695 let rule = MD035HRStyle::new("---".to_string());
696 assert_fix_roundtrip(
697 &rule,
698 "Content\n\n---\n\nMore\n\n---\n\nText",
699 crate::config::MarkdownFlavor::Standard,
700 );
701 }
702
703 #[test]
704 fn test_roundtrip_trailing_newline() {
705 let rule = MD035HRStyle::new("***".to_string());
706 assert_fix_roundtrip(
707 &rule,
708 "Content\n\n---\n\nMore\n",
709 crate::config::MarkdownFlavor::Standard,
710 );
711 }
712}