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 fn default_config_section(&self) -> Option<(String, toml::Value)> {
182 let mut map = toml::map::Map::new();
183 map.insert("style".to_string(), toml::Value::String(self.config.style.clone()));
184 Some((self.name().to_string(), toml::Value::Table(map)))
185 }
186
187 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
188 where
189 Self: Sized,
190 {
191 let style = crate::config::get_rule_config_value::<String>(config, "MD035", "style")
192 .unwrap_or_else(|| "consistent".to_string());
193 Box::new(MD035HRStyle::new(style))
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::lint_context::LintContext;
201
202 #[test]
203 fn test_is_horizontal_rule() {
204 assert!(MD035HRStyle::is_horizontal_rule("---"));
206 assert!(MD035HRStyle::is_horizontal_rule("----"));
207 assert!(MD035HRStyle::is_horizontal_rule("***"));
208 assert!(MD035HRStyle::is_horizontal_rule("****"));
209 assert!(MD035HRStyle::is_horizontal_rule("___"));
210 assert!(MD035HRStyle::is_horizontal_rule("____"));
211 assert!(MD035HRStyle::is_horizontal_rule("- - -"));
212 assert!(MD035HRStyle::is_horizontal_rule("* * *"));
213 assert!(MD035HRStyle::is_horizontal_rule("_ _ _"));
214 assert!(MD035HRStyle::is_horizontal_rule(" --- ")); assert!(!MD035HRStyle::is_horizontal_rule("--")); assert!(!MD035HRStyle::is_horizontal_rule("**"));
219 assert!(!MD035HRStyle::is_horizontal_rule("__"));
220 assert!(!MD035HRStyle::is_horizontal_rule("- -")); assert!(!MD035HRStyle::is_horizontal_rule("* *"));
222 assert!(!MD035HRStyle::is_horizontal_rule("_ _"));
223 assert!(!MD035HRStyle::is_horizontal_rule("text"));
224 assert!(!MD035HRStyle::is_horizontal_rule(""));
225 }
226
227 #[test]
228 fn test_is_potential_setext_heading() {
229 let lines = vec!["Heading 1", "=========", "Content", "Heading 2", "---", "More content"];
230
231 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"];
240 assert!(!MD035HRStyle::is_potential_setext_heading(&lines2, 1)); let lines3 = vec!["***", "---"];
243 assert!(!MD035HRStyle::is_potential_setext_heading(&lines3, 1)); }
245
246 #[test]
247 fn test_most_prevalent_hr_style() {
248 let content = "Content\n\n---\n\nMore\n\n---\n\nText";
250 let lines: Vec<&str> = content.lines().collect();
251 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
252 assert_eq!(
253 MD035HRStyle::most_prevalent_hr_style(&lines, &ctx),
254 Some("---".to_string())
255 );
256
257 let content = "Content\n\n---\n\nMore\n\n***\n\nText\n\n---";
259 let lines: Vec<&str> = content.lines().collect();
260 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
261 assert_eq!(
262 MD035HRStyle::most_prevalent_hr_style(&lines, &ctx),
263 Some("---".to_string())
264 );
265
266 let content = "Content\n\n***\n\nMore\n\n---\n\nText";
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 let content = "Just\nRegular\nContent";
277 let lines: Vec<&str> = content.lines().collect();
278 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
279 assert_eq!(MD035HRStyle::most_prevalent_hr_style(&lines, &ctx), None);
280
281 let content = "Heading\n---\nContent\n\n***";
283 let lines: Vec<&str> = content.lines().collect();
284 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
285 assert_eq!(
286 MD035HRStyle::most_prevalent_hr_style(&lines, &ctx),
287 Some("***".to_string())
288 );
289 }
290
291 #[test]
292 fn test_consistent_style() {
293 let rule = MD035HRStyle::new("consistent".to_string());
294 let content = "Content\n\n---\n\nMore\n\n***\n\nText\n\n---";
295 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
296 let result = rule.check(&ctx).unwrap();
297
298 assert_eq!(result.len(), 1);
300 assert_eq!(result[0].line, 7);
301 assert!(result[0].message.contains("Horizontal rule style should be \"---\""));
302 }
303
304 #[test]
305 fn test_specific_style_dashes() {
306 let rule = MD035HRStyle::new("---".to_string());
307 let content = "Content\n\n***\n\nMore\n\n___\n\nText";
308 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
309 let result = rule.check(&ctx).unwrap();
310
311 assert_eq!(result.len(), 2);
313 assert_eq!(result[0].line, 3);
314 assert_eq!(result[1].line, 7);
315 assert!(result[0].message.contains("Horizontal rule style should be \"---\""));
316 }
317
318 #[test]
319 fn test_indented_horizontal_rule() {
320 let rule = MD035HRStyle::new("---".to_string());
321 let content = "Content\n\n ---\n\nMore";
322 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
323 let result = rule.check(&ctx).unwrap();
324
325 assert_eq!(result.len(), 1);
326 assert_eq!(result[0].line, 3);
327 assert_eq!(result[0].message, "Horizontal rule should not be indented");
328 }
329
330 #[test]
331 fn test_setext_heading_not_flagged() {
332 let rule = MD035HRStyle::new("***".to_string());
333 let content = "Heading\n---\nContent\n***";
334 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
335 let result = rule.check(&ctx).unwrap();
336
337 assert_eq!(result.len(), 0);
339 }
340
341 #[test]
342 fn test_fix_consistent_style() {
343 let rule = MD035HRStyle::new("consistent".to_string());
344 let content = "Content\n\n---\n\nMore\n\n***\n\nText\n\n---";
345 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
346 let fixed = rule.fix(&ctx).unwrap();
347
348 let expected = "Content\n\n---\n\nMore\n\n---\n\nText\n\n---";
349 assert_eq!(fixed, expected);
350 }
351
352 #[test]
353 fn test_fix_specific_style() {
354 let rule = MD035HRStyle::new("***".to_string());
355 let content = "Content\n\n---\n\nMore\n\n___\n\nText";
356 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
357 let fixed = rule.fix(&ctx).unwrap();
358
359 let expected = "Content\n\n***\n\nMore\n\n***\n\nText";
360 assert_eq!(fixed, expected);
361 }
362
363 #[test]
364 fn test_fix_preserves_setext_headings() {
365 let rule = MD035HRStyle::new("***".to_string());
366 let content = "Heading 1\n=========\nHeading 2\n---\nContent\n\n---";
367 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
368 let fixed = rule.fix(&ctx).unwrap();
369
370 let expected = "Heading 1\n=========\nHeading 2\n---\nContent\n\n***";
371 assert_eq!(fixed, expected);
372 }
373
374 #[test]
375 fn test_fix_removes_indentation() {
376 let rule = MD035HRStyle::new("---".to_string());
377 let content = "Content\n\n ***\n\nMore\n\n ___\n\nText";
378 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
379 let fixed = rule.fix(&ctx).unwrap();
380
381 let expected = "Content\n\n---\n\nMore\n\n---\n\nText";
382 assert_eq!(fixed, expected);
383 }
384
385 #[test]
386 fn test_spaced_styles() {
387 let rule = MD035HRStyle::new("* * *".to_string());
388 let content = "Content\n\n- - -\n\nMore\n\n_ _ _\n\nText";
389 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
390 let result = rule.check(&ctx).unwrap();
391
392 assert_eq!(result.len(), 2);
393 assert!(result[0].message.contains("Horizontal rule style should be \"* * *\""));
394 }
395
396 #[test]
397 fn test_empty_style_uses_consistent() {
398 let rule = MD035HRStyle::new("".to_string());
399 let content = "Content\n\n---\n\nMore\n\n***\n\nText";
400 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
401 let result = rule.check(&ctx).unwrap();
402
403 assert_eq!(result.len(), 1);
405 assert_eq!(result[0].line, 7);
406 }
407
408 #[test]
409 fn test_all_hr_styles_consistent() {
410 let rule = MD035HRStyle::new("consistent".to_string());
411 let content = "Content\n---\nMore\n---\nText\n---";
412 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
413 let result = rule.check(&ctx).unwrap();
414
415 assert_eq!(result.len(), 0);
417 }
418
419 #[test]
420 fn test_no_horizontal_rules() {
421 let rule = MD035HRStyle::new("---".to_string());
422 let content = "Just regular content\nNo horizontal rules here";
423 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
424 let result = rule.check(&ctx).unwrap();
425
426 assert_eq!(result.len(), 0);
427 }
428
429 #[test]
430 fn test_mixed_spaced_and_unspaced() {
431 let rule = MD035HRStyle::new("consistent".to_string());
432 let content = "Content\n\n---\n\nMore\n\n- - -\n\nText";
433 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
434 let result = rule.check(&ctx).unwrap();
435
436 assert_eq!(result.len(), 1);
438 assert_eq!(result[0].line, 7);
439 }
440
441 #[test]
442 fn test_trailing_whitespace_in_hr() {
443 let rule = MD035HRStyle::new("---".to_string());
444 let content = "Content\n\n--- \n\nMore";
445 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
446 let result = rule.check(&ctx).unwrap();
447
448 assert_eq!(result.len(), 0);
450 }
451
452 #[test]
453 fn test_hr_in_code_block_not_flagged() {
454 let rule = MD035HRStyle::new("---".to_string());
455 let content =
456 "Text\n\n```bash\n----------------------------------------------------------------------\n```\n\nMore";
457 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
458 let result = rule.check(&ctx).unwrap();
459
460 assert_eq!(result.len(), 0);
462 }
463
464 #[test]
465 fn test_hr_in_code_span_not_flagged() {
466 let rule = MD035HRStyle::new("---".to_string());
467 let content = "Text with inline `---` code span";
468 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
469 let result = rule.check(&ctx).unwrap();
470
471 assert_eq!(result.len(), 0);
473 }
474
475 #[test]
476 fn test_hr_with_extra_characters() {
477 let rule = MD035HRStyle::new("---".to_string());
478 let content = "Content\n-----\nMore\n--------\nText";
479 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
480 let result = rule.check(&ctx).unwrap();
481
482 assert_eq!(result.len(), 0);
484 }
485
486 #[test]
487 fn test_default_config() {
488 let rule = MD035HRStyle::new("consistent".to_string());
489 let (name, config) = rule.default_config_section().unwrap();
490 assert_eq!(name, "MD035");
491
492 let table = config.as_table().unwrap();
493 assert_eq!(table.get("style").unwrap().as_str().unwrap(), "consistent");
494 }
495
496 #[test]
497 fn test_fix_skips_mkdocs_html_markdown() {
498 let rule = MD035HRStyle::new("***".to_string());
501
502 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";
503 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
504
505 let warnings = rule.check(&ctx).unwrap();
507 for w in &warnings {
508 assert_ne!(w.line, 9, "check() should not flag --- inside <div markdown> block");
509 }
510
511 let fixed = rule.fix(&ctx).unwrap();
513 assert!(
514 fixed.contains(" ---"),
515 "fix() should preserve --- inside <div markdown> block, got: {fixed}"
516 );
517 }
518
519 #[test]
520 fn test_is_horizontal_rule_edge_cases() {
521 assert!(MD035HRStyle::is_horizontal_rule("----------"));
523 assert!(MD035HRStyle::is_horizontal_rule("**********"));
524 assert!(MD035HRStyle::is_horizontal_rule("__________"));
525
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
545 assert!(!MD035HRStyle::is_horizontal_rule("-*-"));
547 assert!(!MD035HRStyle::is_horizontal_rule("- * -"));
548 assert!(!MD035HRStyle::is_horizontal_rule("_-_"));
549 assert!(!MD035HRStyle::is_horizontal_rule("*_*"));
550
551 assert!(!MD035HRStyle::is_horizontal_rule("---text"));
553 assert!(!MD035HRStyle::is_horizontal_rule("***text"));
554 assert!(!MD035HRStyle::is_horizontal_rule("- - - text"));
555
556 assert!(!MD035HRStyle::is_horizontal_rule("- -"));
558 assert!(!MD035HRStyle::is_horizontal_rule("* *"));
559 assert!(!MD035HRStyle::is_horizontal_rule("_ _"));
560
561 assert!(!MD035HRStyle::is_horizontal_rule("-a-b-"));
563 assert!(!MD035HRStyle::is_horizontal_rule("*x*x*"));
564
565 assert!(!MD035HRStyle::is_horizontal_rule("-"));
567 assert!(!MD035HRStyle::is_horizontal_rule("*"));
568 assert!(!MD035HRStyle::is_horizontal_rule("_"));
569
570 assert!(MD035HRStyle::is_horizontal_rule("*\t*\t*"));
572 assert!(MD035HRStyle::is_horizontal_rule("-\t-\t-"));
573
574 let long_hr = "-".repeat(200);
576 assert!(MD035HRStyle::is_horizontal_rule(&long_hr));
577 }
578
579 #[test]
580 fn test_frontmatter_not_treated_as_hr() {
581 let rule = MD035HRStyle::new("***".to_string());
582 let content = "---\ntitle: Test\n---\n\n***\n\nContent";
583 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
584 let result = rule.check(&ctx).unwrap();
585
586 assert_eq!(result.len(), 0);
588 }
589
590 #[test]
591 fn test_fix_skips_mkdocs_html_markdown_preserves_outside() {
592 let rule = MD035HRStyle::new("***".to_string());
594
595 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";
596 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
597
598 let fixed = rule.fix(&ctx).unwrap();
599 let lines: Vec<&str> = fixed.lines().collect();
601 assert_eq!(lines[2], "***", "fix() should change --- outside <div markdown> to ***");
602 assert!(
604 fixed.contains(" ---"),
605 "fix() should preserve --- inside <div markdown>"
606 );
607 }
608
609 fn assert_fix_roundtrip(rule: &MD035HRStyle, content: &str, flavor: crate::config::MarkdownFlavor) {
611 let ctx = LintContext::new(content, flavor, None);
612 let fixed = rule.fix(&ctx).unwrap();
613 let ctx2 = LintContext::new(&fixed, flavor, None);
614 let warnings = rule.check(&ctx2).unwrap();
615 assert!(
616 warnings.is_empty(),
617 "fix() output should produce zero check() warnings.\nOriginal:\n{content}\nFixed:\n{fixed}\nWarnings: {warnings:?}"
618 );
619 }
620
621 #[test]
622 fn test_roundtrip_consistent_style() {
623 let rule = MD035HRStyle::new("consistent".to_string());
624 assert_fix_roundtrip(
625 &rule,
626 "Content\n\n---\n\nMore\n\n***\n\nText\n\n---",
627 crate::config::MarkdownFlavor::Standard,
628 );
629 }
630
631 #[test]
632 fn test_roundtrip_specific_style() {
633 let rule = MD035HRStyle::new("***".to_string());
634 assert_fix_roundtrip(
635 &rule,
636 "Content\n\n---\n\nMore\n\n___\n\nText",
637 crate::config::MarkdownFlavor::Standard,
638 );
639 }
640
641 #[test]
642 fn test_roundtrip_indented_hr() {
643 let rule = MD035HRStyle::new("---".to_string());
644 assert_fix_roundtrip(
645 &rule,
646 "Content\n\n ***\n\nMore\n\n ___\n\nText",
647 crate::config::MarkdownFlavor::Standard,
648 );
649 }
650
651 #[test]
652 fn test_roundtrip_setext_headings() {
653 let rule = MD035HRStyle::new("***".to_string());
654 assert_fix_roundtrip(
655 &rule,
656 "Heading 1\n=========\nHeading 2\n---\nContent\n\n---",
657 crate::config::MarkdownFlavor::Standard,
658 );
659 }
660
661 #[test]
662 fn test_roundtrip_frontmatter() {
663 let rule = MD035HRStyle::new("***".to_string());
664 assert_fix_roundtrip(
665 &rule,
666 "---\ntitle: Test\n---\n\n***\n\nContent",
667 crate::config::MarkdownFlavor::Standard,
668 );
669 }
670
671 #[test]
672 fn test_roundtrip_mkdocs_html_markdown() {
673 let rule = MD035HRStyle::new("***".to_string());
674 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";
675 assert_fix_roundtrip(&rule, content, crate::config::MarkdownFlavor::MkDocs);
676 }
677
678 #[test]
679 fn test_roundtrip_spaced_styles() {
680 let rule = MD035HRStyle::new("* * *".to_string());
681 assert_fix_roundtrip(
682 &rule,
683 "Content\n\n- - -\n\nMore\n\n_ _ _\n\nText",
684 crate::config::MarkdownFlavor::Standard,
685 );
686 }
687
688 #[test]
689 fn test_roundtrip_no_warnings() {
690 let rule = MD035HRStyle::new("---".to_string());
691 assert_fix_roundtrip(
692 &rule,
693 "Content\n\n---\n\nMore\n\n---\n\nText",
694 crate::config::MarkdownFlavor::Standard,
695 );
696 }
697
698 #[test]
699 fn test_roundtrip_trailing_newline() {
700 let rule = MD035HRStyle::new("***".to_string());
701 assert_fix_roundtrip(
702 &rule,
703 "Content\n\n---\n\nMore\n",
704 crate::config::MarkdownFlavor::Standard,
705 );
706 }
707}