1use crate::filtered_lines::FilteredLinesExt;
2use crate::utils::LineIndex;
3use crate::utils::range_utils::calculate_line_range;
4use std::collections::HashSet;
5use toml;
6
7use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, Severity};
8use crate::rule_config_serde::RuleConfig;
9
10mod md012_config;
11use md012_config::MD012Config;
12
13#[derive(Debug, Clone, Default)]
18pub struct MD012NoMultipleBlanks {
19 config: MD012Config,
20}
21
22impl MD012NoMultipleBlanks {
23 pub fn new(maximum: usize) -> Self {
24 use crate::types::PositiveUsize;
25 Self {
26 config: MD012Config {
27 maximum: PositiveUsize::new(maximum).unwrap_or(PositiveUsize::from_const(1)),
28 },
29 }
30 }
31
32 pub const fn from_config_struct(config: MD012Config) -> Self {
33 Self { config }
34 }
35
36 fn generate_excess_warnings(
38 &self,
39 blank_start: usize,
40 blank_count: usize,
41 lines: &[&str],
42 lines_to_check: &HashSet<usize>,
43 line_index: &LineIndex,
44 ) -> Vec<LintWarning> {
45 let mut warnings = Vec::new();
46
47 let location = if blank_start == 0 {
48 "at start of file"
49 } else {
50 "between content"
51 };
52
53 for i in self.config.maximum.get()..blank_count {
54 let excess_line_num = blank_start + i;
55 if lines_to_check.contains(&excess_line_num) {
56 let excess_line = excess_line_num + 1;
57 let excess_line_content = lines.get(excess_line_num).unwrap_or(&"");
58 let (start_line, start_col, end_line, end_col) = calculate_line_range(excess_line, excess_line_content);
59 warnings.push(LintWarning {
60 rule_name: Some(self.name().to_string()),
61 severity: Severity::Warning,
62 message: format!("Multiple consecutive blank lines {location}"),
63 line: start_line,
64 column: start_col,
65 end_line,
66 end_column: end_col,
67 fix: Some(Fix {
68 range: {
69 let line_start = line_index.get_line_start_byte(excess_line).unwrap_or(0);
70 let line_end = line_index
71 .get_line_start_byte(excess_line + 1)
72 .unwrap_or(line_start + 1);
73 line_start..line_end
74 },
75 replacement: String::new(),
76 }),
77 });
78 }
79 }
80
81 warnings
82 }
83}
84
85impl Rule for MD012NoMultipleBlanks {
86 fn name(&self) -> &'static str {
87 "MD012"
88 }
89
90 fn description(&self) -> &'static str {
91 "Multiple consecutive blank lines"
92 }
93
94 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
95 let content = ctx.content;
96
97 if content.is_empty() {
99 return Ok(Vec::new());
100 }
101
102 let lines: Vec<&str> = content.lines().collect();
105 let has_potential_blanks = lines
106 .windows(2)
107 .any(|pair| pair[0].trim().is_empty() && pair[1].trim().is_empty());
108
109 let ends_with_multiple_newlines = content.ends_with("\n\n");
112
113 if !has_potential_blanks && !ends_with_multiple_newlines {
114 return Ok(Vec::new());
115 }
116
117 let line_index = &ctx.line_index;
118
119 let mut warnings = Vec::new();
120
121 let mut blank_count = 0;
123 let mut blank_start = 0;
124 let mut last_line_num: Option<usize> = None;
125
126 let mut lines_to_check: HashSet<usize> = HashSet::new();
128
129 for filtered_line in ctx
136 .filtered_lines()
137 .skip_front_matter()
138 .skip_code_blocks()
139 .skip_quarto_divs()
140 .skip_math_blocks()
141 .skip_obsidian_comments()
142 {
143 let line_num = filtered_line.line_num - 1; let line = filtered_line.content;
145
146 if let Some(last) = last_line_num
149 && line_num > last + 1
150 {
151 if blank_count > self.config.maximum.get() {
154 warnings.extend(self.generate_excess_warnings(
155 blank_start,
156 blank_count,
157 &lines,
158 &lines_to_check,
159 line_index,
160 ));
161 }
162 blank_count = 0;
163 lines_to_check.clear();
164 }
165 last_line_num = Some(line_num);
166
167 if line.trim().is_empty() {
168 if blank_count == 0 {
169 blank_start = line_num;
170 }
171 blank_count += 1;
172 if blank_count > self.config.maximum.get() {
174 lines_to_check.insert(line_num);
175 }
176 } else {
177 if blank_count > self.config.maximum.get() {
178 warnings.extend(self.generate_excess_warnings(
179 blank_start,
180 blank_count,
181 &lines,
182 &lines_to_check,
183 line_index,
184 ));
185 }
186 blank_count = 0;
187 lines_to_check.clear();
188 }
189 }
190
191 let last_line_is_blank = lines.last().is_some_and(|l| l.trim().is_empty());
198
199 if blank_count > 0 && last_line_is_blank {
203 let location = "at end of file";
204
205 let report_line = lines.len();
207
208 let fix_start = line_index
211 .get_line_start_byte(report_line - blank_count + 1)
212 .unwrap_or(0);
213 let fix_end = content.len();
214
215 warnings.push(LintWarning {
217 rule_name: Some(self.name().to_string()),
218 severity: Severity::Warning,
219 message: format!("Multiple consecutive blank lines {location}"),
220 line: report_line,
221 column: 1,
222 end_line: report_line,
223 end_column: 1,
224 fix: Some(Fix {
225 range: fix_start..fix_end,
226 replacement: String::new(),
230 }),
231 });
232 }
233
234 Ok(warnings)
235 }
236
237 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
238 let content = ctx.content;
239
240 let mut result = Vec::new();
241 let mut blank_count = 0;
242
243 let mut in_code_block = false;
244 let mut code_block_blanks = Vec::new();
245 let mut in_front_matter = false;
246
247 for filtered_line in ctx.filtered_lines() {
249 let line = filtered_line.content;
250
251 if filtered_line.line_info.in_front_matter {
253 if !in_front_matter {
254 let allowed_blanks = blank_count.min(self.config.maximum.get());
256 if allowed_blanks > 0 {
257 result.extend(vec![""; allowed_blanks]);
258 }
259 blank_count = 0;
260 in_front_matter = true;
261 }
262 result.push(line);
263 continue;
264 } else if in_front_matter {
265 in_front_matter = false;
267 }
268
269 if line.trim_start().starts_with("```") || line.trim_start().starts_with("~~~") {
271 if !in_code_block {
273 let allowed_blanks = blank_count.min(self.config.maximum.get());
274 if allowed_blanks > 0 {
275 result.extend(vec![""; allowed_blanks]);
276 }
277 blank_count = 0;
278 } else {
279 result.append(&mut code_block_blanks);
281 }
282 in_code_block = !in_code_block;
283 result.push(line);
284 continue;
285 }
286
287 if in_code_block {
288 if line.trim().is_empty() {
289 code_block_blanks.push(line);
290 } else {
291 result.append(&mut code_block_blanks);
292 result.push(line);
293 }
294 } else if line.trim().is_empty() {
295 blank_count += 1;
296 } else {
297 let allowed_blanks = blank_count.min(self.config.maximum.get());
299 if allowed_blanks > 0 {
300 result.extend(vec![""; allowed_blanks]);
301 }
302 blank_count = 0;
303 result.push(line);
304 }
305 }
306
307 let mut output = result.join("\n");
311 if content.ends_with('\n') {
312 output.push('\n');
313 }
314
315 Ok(output)
316 }
317
318 fn as_any(&self) -> &dyn std::any::Any {
319 self
320 }
321
322 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
323 ctx.content.is_empty() || !ctx.has_char('\n')
325 }
326
327 fn default_config_section(&self) -> Option<(String, toml::Value)> {
328 let default_config = MD012Config::default();
329 let json_value = serde_json::to_value(&default_config).ok()?;
330 let toml_value = crate::rule_config_serde::json_to_toml_value(&json_value)?;
331
332 if let toml::Value::Table(table) = toml_value {
333 if !table.is_empty() {
334 Some((MD012Config::RULE_NAME.to_string(), toml::Value::Table(table)))
335 } else {
336 None
337 }
338 } else {
339 None
340 }
341 }
342
343 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
344 where
345 Self: Sized,
346 {
347 let rule_config = crate::rule_config_serde::load_rule_config::<MD012Config>(config);
348 Box::new(Self::from_config_struct(rule_config))
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use crate::lint_context::LintContext;
356
357 #[test]
358 fn test_single_blank_line_allowed() {
359 let rule = MD012NoMultipleBlanks::default();
360 let content = "Line 1\n\nLine 2\n\nLine 3";
361 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
362 let result = rule.check(&ctx).unwrap();
363 assert!(result.is_empty());
364 }
365
366 #[test]
367 fn test_multiple_blank_lines_flagged() {
368 let rule = MD012NoMultipleBlanks::default();
369 let content = "Line 1\n\n\nLine 2\n\n\n\nLine 3";
370 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
371 let result = rule.check(&ctx).unwrap();
372 assert_eq!(result.len(), 3); assert_eq!(result[0].line, 3);
374 assert_eq!(result[1].line, 6);
375 assert_eq!(result[2].line, 7);
376 }
377
378 #[test]
379 fn test_custom_maximum() {
380 let rule = MD012NoMultipleBlanks::new(2);
381 let content = "Line 1\n\n\nLine 2\n\n\n\nLine 3";
382 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
383 let result = rule.check(&ctx).unwrap();
384 assert_eq!(result.len(), 1); assert_eq!(result[0].line, 7);
386 }
387
388 #[test]
389 fn test_fix_multiple_blank_lines() {
390 let rule = MD012NoMultipleBlanks::default();
391 let content = "Line 1\n\n\nLine 2\n\n\n\nLine 3";
392 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
393 let fixed = rule.fix(&ctx).unwrap();
394 assert_eq!(fixed, "Line 1\n\nLine 2\n\nLine 3");
395 }
396
397 #[test]
398 fn test_blank_lines_in_code_block() {
399 let rule = MD012NoMultipleBlanks::default();
400 let content = "Before\n\n```\ncode\n\n\n\nmore code\n```\n\nAfter";
401 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
402 let result = rule.check(&ctx).unwrap();
403 assert!(result.is_empty()); }
405
406 #[test]
407 fn test_fix_preserves_code_block_blanks() {
408 let rule = MD012NoMultipleBlanks::default();
409 let content = "Before\n\n\n```\ncode\n\n\n\nmore code\n```\n\n\nAfter";
410 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
411 let fixed = rule.fix(&ctx).unwrap();
412 assert_eq!(fixed, "Before\n\n```\ncode\n\n\n\nmore code\n```\n\nAfter");
413 }
414
415 #[test]
416 fn test_blank_lines_in_front_matter() {
417 let rule = MD012NoMultipleBlanks::default();
418 let content = "---\ntitle: Test\n\n\nauthor: Me\n---\n\nContent";
419 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
420 let result = rule.check(&ctx).unwrap();
421 assert!(result.is_empty()); }
423
424 #[test]
425 fn test_blank_lines_at_start() {
426 let rule = MD012NoMultipleBlanks::default();
427 let content = "\n\n\nContent";
428 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
429 let result = rule.check(&ctx).unwrap();
430 assert_eq!(result.len(), 2);
431 assert!(result[0].message.contains("at start of file"));
432 }
433
434 #[test]
435 fn test_blank_lines_at_end() {
436 let rule = MD012NoMultipleBlanks::default();
437 let content = "Content\n\n\n";
438 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
439 let result = rule.check(&ctx).unwrap();
440 assert_eq!(result.len(), 1);
441 assert!(result[0].message.contains("at end of file"));
442 }
443
444 #[test]
445 fn test_single_blank_at_eof_flagged() {
446 let rule = MD012NoMultipleBlanks::default();
448 let content = "Content\n\n";
449 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
450 let result = rule.check(&ctx).unwrap();
451 assert_eq!(result.len(), 1);
452 assert!(result[0].message.contains("at end of file"));
453 }
454
455 #[test]
456 fn test_whitespace_only_lines() {
457 let rule = MD012NoMultipleBlanks::default();
458 let content = "Line 1\n \n\t\nLine 2";
459 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
460 let result = rule.check(&ctx).unwrap();
461 assert_eq!(result.len(), 1); }
463
464 #[test]
465 fn test_indented_code_blocks() {
466 let rule = MD012NoMultipleBlanks::default();
468 let content = "Text\n\n code\n \n \n more code\n\nText";
469 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
470 let result = rule.check(&ctx).unwrap();
471 assert!(result.is_empty(), "Should not flag blanks inside indented code blocks");
472 }
473
474 #[test]
475 fn test_blanks_in_indented_code_block() {
476 let content = " code line 1\n\n\n code line 2\n";
478 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
479 let rule = MD012NoMultipleBlanks::default();
480 let warnings = rule.check(&ctx).unwrap();
481 assert!(warnings.is_empty(), "Should not flag blanks in indented code");
482 }
483
484 #[test]
485 fn test_blanks_in_indented_code_block_with_heading() {
486 let content = "# Heading\n\n code line 1\n\n\n code line 2\n\nMore text\n";
488 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
489 let rule = MD012NoMultipleBlanks::default();
490 let warnings = rule.check(&ctx).unwrap();
491 assert!(
492 warnings.is_empty(),
493 "Should not flag blanks in indented code after heading"
494 );
495 }
496
497 #[test]
498 fn test_blanks_after_indented_code_block_flagged() {
499 let content = "# Heading\n\n code line\n\n\n\nMore text\n";
501 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
502 let rule = MD012NoMultipleBlanks::default();
503 let warnings = rule.check(&ctx).unwrap();
504 assert_eq!(warnings.len(), 2, "Should flag blanks after indented code block ends");
506 }
507
508 #[test]
509 fn test_fix_with_final_newline() {
510 let rule = MD012NoMultipleBlanks::default();
511 let content = "Line 1\n\n\nLine 2\n";
512 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
513 let fixed = rule.fix(&ctx).unwrap();
514 assert_eq!(fixed, "Line 1\n\nLine 2\n");
515 assert!(fixed.ends_with('\n'));
516 }
517
518 #[test]
519 fn test_empty_content() {
520 let rule = MD012NoMultipleBlanks::default();
521 let content = "";
522 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
523 let result = rule.check(&ctx).unwrap();
524 assert!(result.is_empty());
525 }
526
527 #[test]
528 fn test_nested_code_blocks() {
529 let rule = MD012NoMultipleBlanks::default();
530 let content = "Before\n\n~~~\nouter\n\n```\ninner\n\n\n```\n\n~~~\n\nAfter";
531 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
532 let result = rule.check(&ctx).unwrap();
533 assert!(result.is_empty());
534 }
535
536 #[test]
537 fn test_unclosed_code_block() {
538 let rule = MD012NoMultipleBlanks::default();
539 let content = "Before\n\n```\ncode\n\n\n\nno closing fence";
540 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
541 let result = rule.check(&ctx).unwrap();
542 assert!(result.is_empty()); }
544
545 #[test]
546 fn test_mixed_fence_styles() {
547 let rule = MD012NoMultipleBlanks::default();
548 let content = "Before\n\n```\ncode\n\n\n~~~\n\nAfter";
549 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
550 let result = rule.check(&ctx).unwrap();
551 assert!(result.is_empty()); }
553
554 #[test]
555 fn test_config_from_toml() {
556 let mut config = crate::config::Config::default();
557 let mut rule_config = crate::config::RuleConfig::default();
558 rule_config
559 .values
560 .insert("maximum".to_string(), toml::Value::Integer(3));
561 config.rules.insert("MD012".to_string(), rule_config);
562
563 let rule = MD012NoMultipleBlanks::from_config(&config);
564 let content = "Line 1\n\n\n\nLine 2"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
566 let result = rule.check(&ctx).unwrap();
567 assert!(result.is_empty()); }
569
570 #[test]
571 fn test_blank_lines_between_sections() {
572 let rule = MD012NoMultipleBlanks::default();
573 let content = "# Section 1\n\nContent\n\n\n# Section 2\n\nContent";
574 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
575 let result = rule.check(&ctx).unwrap();
576 assert_eq!(result.len(), 1);
577 assert_eq!(result[0].line, 5);
578 }
579
580 #[test]
581 fn test_fix_preserves_indented_code() {
582 let rule = MD012NoMultipleBlanks::default();
583 let content = "Text\n\n\n code\n \n more code\n\n\nText";
584 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
585 let fixed = rule.fix(&ctx).unwrap();
586 assert_eq!(fixed, "Text\n\n code\n\n more code\n\nText");
588 }
589
590 #[test]
591 fn test_edge_case_only_blanks() {
592 let rule = MD012NoMultipleBlanks::default();
593 let content = "\n\n\n";
594 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
595 let result = rule.check(&ctx).unwrap();
596 assert_eq!(result.len(), 1);
598 assert!(result[0].message.contains("at end of file"));
599 }
600
601 #[test]
604 fn test_blanks_after_fenced_code_block_mid_document() {
605 let rule = MD012NoMultipleBlanks::default();
607 let content = "## Input\n\n```javascript\ncode\n```\n\n\n## Error\n";
608 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
609 let result = rule.check(&ctx).unwrap();
610 assert_eq!(result.len(), 1, "Should detect blanks after code block");
612 assert_eq!(result[0].line, 7, "Warning should be on line 7 (second blank)");
613 assert!(result[0].message.contains("between content"));
614 }
615
616 #[test]
617 fn test_blanks_after_code_block_at_eof() {
618 let rule = MD012NoMultipleBlanks::default();
620 let content = "# Heading\n\n```\ncode\n```\n\n\n";
621 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
622 let result = rule.check(&ctx).unwrap();
623 assert_eq!(result.len(), 1, "Should detect trailing blanks after code block");
625 assert!(result[0].message.contains("at end of file"));
626 }
627
628 #[test]
629 fn test_single_blank_after_code_block_allowed() {
630 let rule = MD012NoMultipleBlanks::default();
632 let content = "## Input\n\n```\ncode\n```\n\n## Output\n";
633 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
634 let result = rule.check(&ctx).unwrap();
635 assert!(result.is_empty(), "Single blank after code block should be allowed");
636 }
637
638 #[test]
639 fn test_multiple_code_blocks_with_blanks() {
640 let rule = MD012NoMultipleBlanks::default();
642 let content = "```\ncode1\n```\n\n\n```\ncode2\n```\n\n\nEnd\n";
643 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
644 let result = rule.check(&ctx).unwrap();
645 assert_eq!(result.len(), 2, "Should detect blanks after both code blocks");
647 }
648
649 #[test]
650 fn test_whitespace_only_lines_after_code_block_at_eof() {
651 let rule = MD012NoMultipleBlanks::default();
654 let content = "```\ncode\n```\n \n \n";
655 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
656 let result = rule.check(&ctx).unwrap();
657 assert_eq!(result.len(), 1, "Should detect whitespace-only trailing blanks");
658 assert!(result[0].message.contains("at end of file"));
659 }
660
661 #[test]
664 fn test_warning_fix_removes_single_trailing_blank() {
665 let rule = MD012NoMultipleBlanks::default();
667 let content = "hello foobar hello.\n\n";
668 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
669 let warnings = rule.check(&ctx).unwrap();
670
671 assert_eq!(warnings.len(), 1);
672 assert!(warnings[0].fix.is_some(), "Warning should have a fix attached");
673
674 let fix = warnings[0].fix.as_ref().unwrap();
675 assert_eq!(fix.replacement, "", "Replacement should be empty");
677
678 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
680 assert_eq!(fixed, "hello foobar hello.\n", "Should end with single newline");
681 }
682
683 #[test]
684 fn test_warning_fix_removes_multiple_trailing_blanks() {
685 let rule = MD012NoMultipleBlanks::default();
686 let content = "content\n\n\n\n";
687 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
688 let warnings = rule.check(&ctx).unwrap();
689
690 assert_eq!(warnings.len(), 1);
691 assert!(warnings[0].fix.is_some());
692
693 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
694 assert_eq!(fixed, "content\n", "Should end with single newline");
695 }
696
697 #[test]
698 fn test_warning_fix_preserves_content_newline() {
699 let rule = MD012NoMultipleBlanks::default();
701 let content = "line1\nline2\n\n";
702 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
703 let warnings = rule.check(&ctx).unwrap();
704
705 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
706 assert_eq!(fixed, "line1\nline2\n", "Should preserve all content lines");
707 }
708
709 #[test]
710 fn test_warning_fix_mid_document_blanks() {
711 let rule = MD012NoMultipleBlanks::default();
713 let content = "# Heading\n\n\n\nParagraph\n";
715 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
716 let warnings = rule.check(&ctx).unwrap();
717
718 assert_eq!(warnings.len(), 2, "Should have 2 warnings for 2 extra blank lines");
720 assert!(warnings[0].fix.is_some());
721 assert!(warnings[1].fix.is_some());
722
723 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
724 assert_eq!(fixed, "# Heading\n\nParagraph\n", "Should reduce to single blank");
725 }
726
727 #[test]
730 fn test_blank_lines_in_quarto_callout() {
731 let rule = MD012NoMultipleBlanks::default();
733 let content = "# Heading\n\n::: {.callout-note}\nNote content\n\n\nMore content\n:::\n\nAfter";
734 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
735 let result = rule.check(&ctx).unwrap();
736 assert!(result.is_empty(), "Should not flag blanks inside Quarto callouts");
737 }
738
739 #[test]
740 fn test_blank_lines_in_quarto_div() {
741 let rule = MD012NoMultipleBlanks::default();
743 let content = "Text\n\n::: {.bordered}\nContent\n\n\nMore\n:::\n\nText";
744 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
745 let result = rule.check(&ctx).unwrap();
746 assert!(result.is_empty(), "Should not flag blanks inside Quarto divs");
747 }
748
749 #[test]
750 fn test_blank_lines_outside_quarto_div_flagged() {
751 let rule = MD012NoMultipleBlanks::default();
753 let content = "Text\n\n\n::: {.callout-note}\nNote\n:::\n\n\nMore";
754 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
755 let result = rule.check(&ctx).unwrap();
756 assert!(!result.is_empty(), "Should flag blanks outside Quarto divs");
757 }
758
759 #[test]
760 fn test_quarto_divs_ignored_in_standard_flavor() {
761 let rule = MD012NoMultipleBlanks::default();
763 let content = "::: {.callout-note}\nNote content\n\n\nMore content\n:::\n";
764 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
765 let result = rule.check(&ctx).unwrap();
766 assert!(!result.is_empty(), "Standard flavor should flag blanks in 'div'");
768 }
769}