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 mut warnings = Vec::new();
100 let lines = ctx.raw_lines();
101
102 let expected_style = if self.config.style.is_empty() || self.config.style == "consistent" {
104 Self::most_prevalent_hr_style(lines, ctx).unwrap_or_else(|| "---".to_string())
105 } else {
106 self.config.style.clone()
107 };
108
109 for (i, line) in lines.iter().enumerate() {
110 if let Some(line_info) = ctx.lines.get(i)
112 && (line_info.in_front_matter || line_info.in_code_block || line_info.in_mkdocs_html_markdown)
113 {
114 continue;
115 }
116
117 if Self::is_potential_setext_heading(lines, i) {
119 continue;
120 }
121
122 if Self::is_horizontal_rule(line) {
123 let has_indentation = line.len() > line.trim_start().len();
125 let style_mismatch = line.trim() != expected_style;
126
127 if style_mismatch || has_indentation {
128 let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);
130
131 warnings.push(LintWarning {
132 rule_name: Some(self.name().to_string()),
133 line: start_line,
134 column: start_col,
135 end_line,
136 end_column: end_col,
137 message: if has_indentation {
138 "Horizontal rule should not be indented".to_string()
139 } else {
140 format!("Horizontal rule style should be \"{expected_style}\"")
141 },
142 severity: Severity::Warning,
143 fix: Some(Fix::new(
144 ctx.line_column_byte_range_with_length(i + 1, 1, line.chars().count()),
145 expected_style.clone(),
146 )),
147 });
148 }
149 }
150 }
151
152 Ok(warnings)
153 }
154
155 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
156 if self.should_skip(ctx) {
157 return Ok(ctx.content.to_string());
158 }
159 let warnings = self.check(ctx)?;
160 if warnings.is_empty() {
161 return Ok(ctx.content.to_string());
162 }
163 let warnings =
164 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
165 crate::utils::fix_utils::apply_warning_fixes(ctx.content, &warnings)
166 .map_err(crate::rule::LintError::InvalidInput)
167 }
168
169 fn as_any(&self) -> &dyn std::any::Any {
170 self
171 }
172
173 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
175 ctx.content.is_empty() || (!ctx.has_char('-') && !ctx.has_char('*') && !ctx.has_char('_'))
177 }
178
179 crate::impl_rule_config_methods!(MD035Config);
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::lint_context::LintContext;
186
187 #[test]
188 fn test_is_horizontal_rule() {
189 assert!(MD035HRStyle::is_horizontal_rule("---"));
191 assert!(MD035HRStyle::is_horizontal_rule("----"));
192 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(" --- ")); assert!(!MD035HRStyle::is_horizontal_rule("--")); assert!(!MD035HRStyle::is_horizontal_rule("**"));
204 assert!(!MD035HRStyle::is_horizontal_rule("__"));
205 assert!(!MD035HRStyle::is_horizontal_rule("- -")); assert!(!MD035HRStyle::is_horizontal_rule("* *"));
207 assert!(!MD035HRStyle::is_horizontal_rule("_ _"));
208 assert!(!MD035HRStyle::is_horizontal_rule("text"));
209 assert!(!MD035HRStyle::is_horizontal_rule(""));
210 }
211
212 #[test]
213 fn test_is_potential_setext_heading() {
214 let lines = vec!["Heading 1", "=========", "Content", "Heading 2", "---", "More content"];
215
216 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"];
225 assert!(!MD035HRStyle::is_potential_setext_heading(&lines2, 1)); let lines3 = vec!["***", "---"];
228 assert!(!MD035HRStyle::is_potential_setext_heading(&lines3, 1)); }
230
231 #[test]
232 fn test_most_prevalent_hr_style() {
233 let content = "Content\n\n---\n\nMore\n\n---\n\nText";
235 let lines: Vec<&str> = content.lines().collect();
236 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
237 assert_eq!(
238 MD035HRStyle::most_prevalent_hr_style(&lines, &ctx),
239 Some("---".to_string())
240 );
241
242 let content = "Content\n\n---\n\nMore\n\n***\n\nText\n\n---";
244 let lines: Vec<&str> = content.lines().collect();
245 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
246 assert_eq!(
247 MD035HRStyle::most_prevalent_hr_style(&lines, &ctx),
248 Some("---".to_string())
249 );
250
251 let content = "Content\n\n***\n\nMore\n\n---\n\nText";
253 let lines: Vec<&str> = content.lines().collect();
254 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
255 assert_eq!(
256 MD035HRStyle::most_prevalent_hr_style(&lines, &ctx),
257 Some("***".to_string())
258 );
259
260 let content = "Just\nRegular\nContent";
262 let lines: Vec<&str> = content.lines().collect();
263 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
264 assert_eq!(MD035HRStyle::most_prevalent_hr_style(&lines, &ctx), None);
265
266 let content = "Heading\n---\nContent\n\n***";
268 let lines: Vec<&str> = content.lines().collect();
269 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
270 assert_eq!(
271 MD035HRStyle::most_prevalent_hr_style(&lines, &ctx),
272 Some("***".to_string())
273 );
274 }
275
276 #[test]
277 fn test_consistent_style() {
278 let rule = MD035HRStyle::new("consistent".to_string());
279 let content = "Content\n\n---\n\nMore\n\n***\n\nText\n\n---";
280 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
281 let result = rule.check(&ctx).unwrap();
282
283 assert_eq!(result.len(), 1);
285 assert_eq!(result[0].line, 7);
286 assert!(result[0].message.contains("Horizontal rule style should be \"---\""));
287 }
288
289 #[test]
290 fn test_specific_style_dashes() {
291 let rule = MD035HRStyle::new("---".to_string());
292 let content = "Content\n\n***\n\nMore\n\n___\n\nText";
293 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
294 let result = rule.check(&ctx).unwrap();
295
296 assert_eq!(result.len(), 2);
298 assert_eq!(result[0].line, 3);
299 assert_eq!(result[1].line, 7);
300 assert!(result[0].message.contains("Horizontal rule style should be \"---\""));
301 }
302
303 #[test]
304 fn test_indented_horizontal_rule() {
305 let rule = MD035HRStyle::new("---".to_string());
306 let content = "Content\n\n ---\n\nMore";
307 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
308 let result = rule.check(&ctx).unwrap();
309
310 assert_eq!(result.len(), 1);
311 assert_eq!(result[0].line, 3);
312 assert_eq!(result[0].message, "Horizontal rule should not be indented");
313 }
314
315 #[test]
316 fn test_setext_heading_not_flagged() {
317 let rule = MD035HRStyle::new("***".to_string());
318 let content = "Heading\n---\nContent\n***";
319 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
320 let result = rule.check(&ctx).unwrap();
321
322 assert_eq!(result.len(), 0);
324 }
325
326 #[test]
327 fn test_fix_consistent_style() {
328 let rule = MD035HRStyle::new("consistent".to_string());
329 let content = "Content\n\n---\n\nMore\n\n***\n\nText\n\n---";
330 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
331 let fixed = rule.fix(&ctx).unwrap();
332
333 let expected = "Content\n\n---\n\nMore\n\n---\n\nText\n\n---";
334 assert_eq!(fixed, expected);
335 }
336
337 #[test]
338 fn test_fix_specific_style() {
339 let rule = MD035HRStyle::new("***".to_string());
340 let content = "Content\n\n---\n\nMore\n\n___\n\nText";
341 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
342 let fixed = rule.fix(&ctx).unwrap();
343
344 let expected = "Content\n\n***\n\nMore\n\n***\n\nText";
345 assert_eq!(fixed, expected);
346 }
347
348 #[test]
349 fn test_fix_preserves_setext_headings() {
350 let rule = MD035HRStyle::new("***".to_string());
351 let content = "Heading 1\n=========\nHeading 2\n---\nContent\n\n---";
352 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
353 let fixed = rule.fix(&ctx).unwrap();
354
355 let expected = "Heading 1\n=========\nHeading 2\n---\nContent\n\n***";
356 assert_eq!(fixed, expected);
357 }
358
359 #[test]
360 fn test_fix_removes_indentation() {
361 let rule = MD035HRStyle::new("---".to_string());
362 let content = "Content\n\n ***\n\nMore\n\n ___\n\nText";
363 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
364 let fixed = rule.fix(&ctx).unwrap();
365
366 let expected = "Content\n\n---\n\nMore\n\n---\n\nText";
367 assert_eq!(fixed, expected);
368 }
369
370 #[test]
371 fn test_spaced_styles() {
372 let rule = MD035HRStyle::new("* * *".to_string());
373 let content = "Content\n\n- - -\n\nMore\n\n_ _ _\n\nText";
374 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
375 let result = rule.check(&ctx).unwrap();
376
377 assert_eq!(result.len(), 2);
378 assert!(result[0].message.contains("Horizontal rule style should be \"* * *\""));
379 }
380
381 #[test]
382 fn test_empty_style_uses_consistent() {
383 let rule = MD035HRStyle::new("".to_string());
384 let content = "Content\n\n---\n\nMore\n\n***\n\nText";
385 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
386 let result = rule.check(&ctx).unwrap();
387
388 assert_eq!(result.len(), 1);
390 assert_eq!(result[0].line, 7);
391 }
392
393 #[test]
394 fn test_all_hr_styles_consistent() {
395 let rule = MD035HRStyle::new("consistent".to_string());
396 let content = "Content\n---\nMore\n---\nText\n---";
397 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
398 let result = rule.check(&ctx).unwrap();
399
400 assert_eq!(result.len(), 0);
402 }
403
404 #[test]
405 fn test_no_horizontal_rules() {
406 let rule = MD035HRStyle::new("---".to_string());
407 let content = "Just regular content\nNo horizontal rules here";
408 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
409 let result = rule.check(&ctx).unwrap();
410
411 assert_eq!(result.len(), 0);
412 }
413
414 #[test]
415 fn test_mixed_spaced_and_unspaced() {
416 let rule = MD035HRStyle::new("consistent".to_string());
417 let content = "Content\n\n---\n\nMore\n\n- - -\n\nText";
418 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
419 let result = rule.check(&ctx).unwrap();
420
421 assert_eq!(result.len(), 1);
423 assert_eq!(result[0].line, 7);
424 }
425
426 #[test]
427 fn test_trailing_whitespace_in_hr() {
428 let rule = MD035HRStyle::new("---".to_string());
429 let content = "Content\n\n--- \n\nMore";
430 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
431 let result = rule.check(&ctx).unwrap();
432
433 assert_eq!(result.len(), 0);
435 }
436
437 #[test]
438 fn test_hr_in_code_block_not_flagged() {
439 let rule = MD035HRStyle::new("---".to_string());
440 let content =
441 "Text\n\n```bash\n----------------------------------------------------------------------\n```\n\nMore";
442 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
443 let result = rule.check(&ctx).unwrap();
444
445 assert_eq!(result.len(), 0);
447 }
448
449 #[test]
450 fn test_hr_in_code_span_not_flagged() {
451 let rule = MD035HRStyle::new("---".to_string());
452 let content = "Text with inline `---` code span";
453 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
454 let result = rule.check(&ctx).unwrap();
455
456 assert_eq!(result.len(), 0);
458 }
459
460 #[test]
461 fn test_hr_with_extra_characters() {
462 let rule = MD035HRStyle::new("---".to_string());
463 let content = "Content\n-----\nMore\n--------\nText";
464 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
465 let result = rule.check(&ctx).unwrap();
466
467 assert_eq!(result.len(), 0);
469 }
470
471 #[test]
472 fn test_default_config() {
473 let style = |rule: &MD035HRStyle| {
477 let (name, config) = rule.default_config_section().unwrap();
478 assert_eq!(name, "MD035");
479 config
480 .as_table()
481 .unwrap()
482 .get("style")
483 .unwrap()
484 .as_str()
485 .unwrap()
486 .to_string()
487 };
488
489 assert_eq!(style(&MD035HRStyle::default()), "consistent");
492 assert_eq!(
493 style(&MD035HRStyle::new("***".to_string())),
494 "consistent",
495 "a configured instance must still publish the default"
496 );
497 }
498
499 #[test]
500 fn test_fix_skips_mkdocs_html_markdown() {
501 let rule = MD035HRStyle::new("***".to_string());
504
505 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";
506 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
507
508 let warnings = rule.check(&ctx).unwrap();
510 for w in &warnings {
511 assert_ne!(w.line, 9, "check() should not flag --- inside <div markdown> block");
512 }
513
514 let fixed = rule.fix(&ctx).unwrap();
516 assert!(
517 fixed.contains(" ---"),
518 "fix() should preserve --- inside <div markdown> block, got: {fixed}"
519 );
520 }
521
522 #[test]
523 fn test_is_horizontal_rule_edge_cases() {
524 assert!(MD035HRStyle::is_horizontal_rule("----------"));
526 assert!(MD035HRStyle::is_horizontal_rule("**********"));
527 assert!(MD035HRStyle::is_horizontal_rule("__________"));
528
529 assert!(MD035HRStyle::is_horizontal_rule("- - - -"));
531 assert!(MD035HRStyle::is_horizontal_rule("* * * * *"));
532 assert!(MD035HRStyle::is_horizontal_rule("_ _ _ _ _ _"));
533
534 assert!(MD035HRStyle::is_horizontal_rule("* * *"));
536 assert!(MD035HRStyle::is_horizontal_rule("- - -"));
537 assert!(MD035HRStyle::is_horizontal_rule("_ _ _"));
538
539 assert!(MD035HRStyle::is_horizontal_rule("--- "));
541 assert!(MD035HRStyle::is_horizontal_rule("*** "));
542 assert!(MD035HRStyle::is_horizontal_rule("___ "));
543
544 assert!(MD035HRStyle::is_horizontal_rule("- - - "));
546 assert!(MD035HRStyle::is_horizontal_rule("* * * "));
547
548 assert!(!MD035HRStyle::is_horizontal_rule("-*-"));
550 assert!(!MD035HRStyle::is_horizontal_rule("- * -"));
551 assert!(!MD035HRStyle::is_horizontal_rule("_-_"));
552 assert!(!MD035HRStyle::is_horizontal_rule("*_*"));
553
554 assert!(!MD035HRStyle::is_horizontal_rule("---text"));
556 assert!(!MD035HRStyle::is_horizontal_rule("***text"));
557 assert!(!MD035HRStyle::is_horizontal_rule("- - - text"));
558
559 assert!(!MD035HRStyle::is_horizontal_rule("- -"));
561 assert!(!MD035HRStyle::is_horizontal_rule("* *"));
562 assert!(!MD035HRStyle::is_horizontal_rule("_ _"));
563
564 assert!(!MD035HRStyle::is_horizontal_rule("-a-b-"));
566 assert!(!MD035HRStyle::is_horizontal_rule("*x*x*"));
567
568 assert!(!MD035HRStyle::is_horizontal_rule("-"));
570 assert!(!MD035HRStyle::is_horizontal_rule("*"));
571 assert!(!MD035HRStyle::is_horizontal_rule("_"));
572
573 assert!(MD035HRStyle::is_horizontal_rule("*\t*\t*"));
575 assert!(MD035HRStyle::is_horizontal_rule("-\t-\t-"));
576
577 let long_hr = "-".repeat(200);
579 assert!(MD035HRStyle::is_horizontal_rule(&long_hr));
580 }
581
582 #[test]
583 fn test_frontmatter_not_treated_as_hr() {
584 let rule = MD035HRStyle::new("***".to_string());
585 let content = "---\ntitle: Test\n---\n\n***\n\nContent";
586 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
587 let result = rule.check(&ctx).unwrap();
588
589 assert_eq!(result.len(), 0);
591 }
592
593 #[test]
594 fn test_fix_skips_mkdocs_html_markdown_preserves_outside() {
595 let rule = MD035HRStyle::new("***".to_string());
597
598 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";
599 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
600
601 let fixed = rule.fix(&ctx).unwrap();
602 let lines: Vec<&str> = fixed.lines().collect();
604 assert_eq!(lines[2], "***", "fix() should change --- outside <div markdown> to ***");
605 assert!(
607 fixed.contains(" ---"),
608 "fix() should preserve --- inside <div markdown>"
609 );
610 }
611
612 fn assert_fix_roundtrip(rule: &MD035HRStyle, content: &str, flavor: crate::config::MarkdownFlavor) {
614 let ctx = LintContext::new(content, flavor, None);
615 let fixed = rule.fix(&ctx).unwrap();
616 let ctx2 = LintContext::new(&fixed, flavor, None);
617 let warnings = rule.check(&ctx2).unwrap();
618 assert!(
619 warnings.is_empty(),
620 "fix() output should produce zero check() warnings.\nOriginal:\n{content}\nFixed:\n{fixed}\nWarnings: {warnings:?}"
621 );
622 }
623
624 #[test]
625 fn test_roundtrip_consistent_style() {
626 let rule = MD035HRStyle::new("consistent".to_string());
627 assert_fix_roundtrip(
628 &rule,
629 "Content\n\n---\n\nMore\n\n***\n\nText\n\n---",
630 crate::config::MarkdownFlavor::Standard,
631 );
632 }
633
634 #[test]
635 fn test_roundtrip_specific_style() {
636 let rule = MD035HRStyle::new("***".to_string());
637 assert_fix_roundtrip(
638 &rule,
639 "Content\n\n---\n\nMore\n\n___\n\nText",
640 crate::config::MarkdownFlavor::Standard,
641 );
642 }
643
644 #[test]
645 fn test_roundtrip_indented_hr() {
646 let rule = MD035HRStyle::new("---".to_string());
647 assert_fix_roundtrip(
648 &rule,
649 "Content\n\n ***\n\nMore\n\n ___\n\nText",
650 crate::config::MarkdownFlavor::Standard,
651 );
652 }
653
654 #[test]
655 fn test_roundtrip_setext_headings() {
656 let rule = MD035HRStyle::new("***".to_string());
657 assert_fix_roundtrip(
658 &rule,
659 "Heading 1\n=========\nHeading 2\n---\nContent\n\n---",
660 crate::config::MarkdownFlavor::Standard,
661 );
662 }
663
664 #[test]
665 fn test_roundtrip_frontmatter() {
666 let rule = MD035HRStyle::new("***".to_string());
667 assert_fix_roundtrip(
668 &rule,
669 "---\ntitle: Test\n---\n\n***\n\nContent",
670 crate::config::MarkdownFlavor::Standard,
671 );
672 }
673
674 #[test]
675 fn test_roundtrip_mkdocs_html_markdown() {
676 let rule = MD035HRStyle::new("***".to_string());
677 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";
678 assert_fix_roundtrip(&rule, content, crate::config::MarkdownFlavor::MkDocs);
679 }
680
681 #[test]
682 fn test_roundtrip_spaced_styles() {
683 let rule = MD035HRStyle::new("* * *".to_string());
684 assert_fix_roundtrip(
685 &rule,
686 "Content\n\n- - -\n\nMore\n\n_ _ _\n\nText",
687 crate::config::MarkdownFlavor::Standard,
688 );
689 }
690
691 #[test]
692 fn test_roundtrip_no_warnings() {
693 let rule = MD035HRStyle::new("---".to_string());
694 assert_fix_roundtrip(
695 &rule,
696 "Content\n\n---\n\nMore\n\n---\n\nText",
697 crate::config::MarkdownFlavor::Standard,
698 );
699 }
700
701 #[test]
702 fn test_roundtrip_trailing_newline() {
703 let rule = MD035HRStyle::new("***".to_string());
704 assert_fix_roundtrip(
705 &rule,
706 "Content\n\n---\n\nMore\n",
707 crate::config::MarkdownFlavor::Standard,
708 );
709 }
710}