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 = ctx.raw_lines();
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 .skip_pymdown_blocks()
143 {
144 let line_num = filtered_line.line_num - 1; let line = filtered_line.content;
146
147 if let Some(last) = last_line_num
150 && line_num > last + 1
151 {
152 if blank_count > self.config.maximum.get() {
155 warnings.extend(self.generate_excess_warnings(
156 blank_start,
157 blank_count,
158 lines,
159 &lines_to_check,
160 line_index,
161 ));
162 }
163 blank_count = 0;
164 lines_to_check.clear();
165 }
166 last_line_num = Some(line_num);
167
168 if line.trim().is_empty() {
169 if blank_count == 0 {
170 blank_start = line_num;
171 }
172 blank_count += 1;
173 if blank_count > self.config.maximum.get() {
175 lines_to_check.insert(line_num);
176 }
177 } else {
178 if blank_count > self.config.maximum.get() {
179 warnings.extend(self.generate_excess_warnings(
180 blank_start,
181 blank_count,
182 lines,
183 &lines_to_check,
184 line_index,
185 ));
186 }
187 blank_count = 0;
188 lines_to_check.clear();
189 }
190 }
191
192 let last_line_is_blank = lines.last().is_some_and(|l| l.trim().is_empty());
199
200 if blank_count > 0 && last_line_is_blank {
204 let location = "at end of file";
205
206 let report_line = lines.len();
208
209 let fix_start = line_index
212 .get_line_start_byte(report_line - blank_count + 1)
213 .unwrap_or(0);
214 let fix_end = content.len();
215
216 warnings.push(LintWarning {
218 rule_name: Some(self.name().to_string()),
219 severity: Severity::Warning,
220 message: format!("Multiple consecutive blank lines {location}"),
221 line: report_line,
222 column: 1,
223 end_line: report_line,
224 end_column: 1,
225 fix: Some(Fix {
226 range: fix_start..fix_end,
227 replacement: String::new(),
231 }),
232 });
233 }
234
235 Ok(warnings)
236 }
237
238 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
239 let content = ctx.content;
240
241 let mut result = Vec::new();
242 let mut blank_count = 0;
243
244 let mut in_code_block = false;
245 let mut code_block_blanks = Vec::new();
246 let mut in_front_matter = false;
247
248 for filtered_line in ctx.filtered_lines() {
250 let line = filtered_line.content;
251
252 if filtered_line.line_info.in_front_matter {
254 if !in_front_matter {
255 let allowed_blanks = blank_count.min(self.config.maximum.get());
257 if allowed_blanks > 0 {
258 result.extend(vec![""; allowed_blanks]);
259 }
260 blank_count = 0;
261 in_front_matter = true;
262 }
263 result.push(line);
264 continue;
265 } else if in_front_matter {
266 in_front_matter = false;
268 }
269
270 if line.trim_start().starts_with("```") || line.trim_start().starts_with("~~~") {
272 if !in_code_block {
274 let allowed_blanks = blank_count.min(self.config.maximum.get());
275 if allowed_blanks > 0 {
276 result.extend(vec![""; allowed_blanks]);
277 }
278 blank_count = 0;
279 } else {
280 result.append(&mut code_block_blanks);
282 }
283 in_code_block = !in_code_block;
284 result.push(line);
285 continue;
286 }
287
288 if in_code_block {
289 if line.trim().is_empty() {
290 code_block_blanks.push(line);
291 } else {
292 result.append(&mut code_block_blanks);
293 result.push(line);
294 }
295 } else if line.trim().is_empty() {
296 blank_count += 1;
297 } else {
298 let allowed_blanks = blank_count.min(self.config.maximum.get());
300 if allowed_blanks > 0 {
301 result.extend(vec![""; allowed_blanks]);
302 }
303 blank_count = 0;
304 result.push(line);
305 }
306 }
307
308 let mut output = result.join("\n");
312 if content.ends_with('\n') {
313 output.push('\n');
314 }
315
316 Ok(output)
317 }
318
319 fn as_any(&self) -> &dyn std::any::Any {
320 self
321 }
322
323 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
324 ctx.content.is_empty() || !ctx.has_char('\n')
326 }
327
328 fn default_config_section(&self) -> Option<(String, toml::Value)> {
329 let default_config = MD012Config::default();
330 let json_value = serde_json::to_value(&default_config).ok()?;
331 let toml_value = crate::rule_config_serde::json_to_toml_value(&json_value)?;
332
333 if let toml::Value::Table(table) = toml_value {
334 if !table.is_empty() {
335 Some((MD012Config::RULE_NAME.to_string(), toml::Value::Table(table)))
336 } else {
337 None
338 }
339 } else {
340 None
341 }
342 }
343
344 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
345 where
346 Self: Sized,
347 {
348 let rule_config = crate::rule_config_serde::load_rule_config::<MD012Config>(config);
349 Box::new(Self::from_config_struct(rule_config))
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use crate::lint_context::LintContext;
357
358 #[test]
359 fn test_single_blank_line_allowed() {
360 let rule = MD012NoMultipleBlanks::default();
361 let content = "Line 1\n\nLine 2\n\nLine 3";
362 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
363 let result = rule.check(&ctx).unwrap();
364 assert!(result.is_empty());
365 }
366
367 #[test]
368 fn test_multiple_blank_lines_flagged() {
369 let rule = MD012NoMultipleBlanks::default();
370 let content = "Line 1\n\n\nLine 2\n\n\n\nLine 3";
371 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
372 let result = rule.check(&ctx).unwrap();
373 assert_eq!(result.len(), 3); assert_eq!(result[0].line, 3);
375 assert_eq!(result[1].line, 6);
376 assert_eq!(result[2].line, 7);
377 }
378
379 #[test]
380 fn test_custom_maximum() {
381 let rule = MD012NoMultipleBlanks::new(2);
382 let content = "Line 1\n\n\nLine 2\n\n\n\nLine 3";
383 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
384 let result = rule.check(&ctx).unwrap();
385 assert_eq!(result.len(), 1); assert_eq!(result[0].line, 7);
387 }
388
389 #[test]
390 fn test_fix_multiple_blank_lines() {
391 let rule = MD012NoMultipleBlanks::default();
392 let content = "Line 1\n\n\nLine 2\n\n\n\nLine 3";
393 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
394 let fixed = rule.fix(&ctx).unwrap();
395 assert_eq!(fixed, "Line 1\n\nLine 2\n\nLine 3");
396 }
397
398 #[test]
399 fn test_blank_lines_in_code_block() {
400 let rule = MD012NoMultipleBlanks::default();
401 let content = "Before\n\n```\ncode\n\n\n\nmore code\n```\n\nAfter";
402 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
403 let result = rule.check(&ctx).unwrap();
404 assert!(result.is_empty()); }
406
407 #[test]
408 fn test_fix_preserves_code_block_blanks() {
409 let rule = MD012NoMultipleBlanks::default();
410 let content = "Before\n\n\n```\ncode\n\n\n\nmore code\n```\n\n\nAfter";
411 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
412 let fixed = rule.fix(&ctx).unwrap();
413 assert_eq!(fixed, "Before\n\n```\ncode\n\n\n\nmore code\n```\n\nAfter");
414 }
415
416 #[test]
417 fn test_blank_lines_in_front_matter() {
418 let rule = MD012NoMultipleBlanks::default();
419 let content = "---\ntitle: Test\n\n\nauthor: Me\n---\n\nContent";
420 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
421 let result = rule.check(&ctx).unwrap();
422 assert!(result.is_empty()); }
424
425 #[test]
426 fn test_blank_lines_at_start() {
427 let rule = MD012NoMultipleBlanks::default();
428 let content = "\n\n\nContent";
429 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
430 let result = rule.check(&ctx).unwrap();
431 assert_eq!(result.len(), 2);
432 assert!(result[0].message.contains("at start of file"));
433 }
434
435 #[test]
436 fn test_blank_lines_at_end() {
437 let rule = MD012NoMultipleBlanks::default();
438 let content = "Content\n\n\n";
439 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
440 let result = rule.check(&ctx).unwrap();
441 assert_eq!(result.len(), 1);
442 assert!(result[0].message.contains("at end of file"));
443 }
444
445 #[test]
446 fn test_single_blank_at_eof_flagged() {
447 let rule = MD012NoMultipleBlanks::default();
449 let content = "Content\n\n";
450 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
451 let result = rule.check(&ctx).unwrap();
452 assert_eq!(result.len(), 1);
453 assert!(result[0].message.contains("at end of file"));
454 }
455
456 #[test]
457 fn test_whitespace_only_lines() {
458 let rule = MD012NoMultipleBlanks::default();
459 let content = "Line 1\n \n\t\nLine 2";
460 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
461 let result = rule.check(&ctx).unwrap();
462 assert_eq!(result.len(), 1); }
464
465 #[test]
466 fn test_indented_code_blocks() {
467 let rule = MD012NoMultipleBlanks::default();
469 let content = "Text\n\n code\n \n \n more code\n\nText";
470 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
471 let result = rule.check(&ctx).unwrap();
472 assert!(result.is_empty(), "Should not flag blanks inside indented code blocks");
473 }
474
475 #[test]
476 fn test_blanks_in_indented_code_block() {
477 let content = " code line 1\n\n\n code line 2\n";
479 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
480 let rule = MD012NoMultipleBlanks::default();
481 let warnings = rule.check(&ctx).unwrap();
482 assert!(warnings.is_empty(), "Should not flag blanks in indented code");
483 }
484
485 #[test]
486 fn test_blanks_in_indented_code_block_with_heading() {
487 let content = "# Heading\n\n code line 1\n\n\n code line 2\n\nMore text\n";
489 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
490 let rule = MD012NoMultipleBlanks::default();
491 let warnings = rule.check(&ctx).unwrap();
492 assert!(
493 warnings.is_empty(),
494 "Should not flag blanks in indented code after heading"
495 );
496 }
497
498 #[test]
499 fn test_blanks_after_indented_code_block_flagged() {
500 let content = "# Heading\n\n code line\n\n\n\nMore text\n";
502 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
503 let rule = MD012NoMultipleBlanks::default();
504 let warnings = rule.check(&ctx).unwrap();
505 assert_eq!(warnings.len(), 2, "Should flag blanks after indented code block ends");
507 }
508
509 #[test]
510 fn test_fix_with_final_newline() {
511 let rule = MD012NoMultipleBlanks::default();
512 let content = "Line 1\n\n\nLine 2\n";
513 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
514 let fixed = rule.fix(&ctx).unwrap();
515 assert_eq!(fixed, "Line 1\n\nLine 2\n");
516 assert!(fixed.ends_with('\n'));
517 }
518
519 #[test]
520 fn test_empty_content() {
521 let rule = MD012NoMultipleBlanks::default();
522 let content = "";
523 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
524 let result = rule.check(&ctx).unwrap();
525 assert!(result.is_empty());
526 }
527
528 #[test]
529 fn test_nested_code_blocks() {
530 let rule = MD012NoMultipleBlanks::default();
531 let content = "Before\n\n~~~\nouter\n\n```\ninner\n\n\n```\n\n~~~\n\nAfter";
532 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
533 let result = rule.check(&ctx).unwrap();
534 assert!(result.is_empty());
535 }
536
537 #[test]
538 fn test_unclosed_code_block() {
539 let rule = MD012NoMultipleBlanks::default();
540 let content = "Before\n\n```\ncode\n\n\n\nno closing fence";
541 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
542 let result = rule.check(&ctx).unwrap();
543 assert!(result.is_empty()); }
545
546 #[test]
547 fn test_mixed_fence_styles() {
548 let rule = MD012NoMultipleBlanks::default();
549 let content = "Before\n\n```\ncode\n\n\n~~~\n\nAfter";
550 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
551 let result = rule.check(&ctx).unwrap();
552 assert!(result.is_empty()); }
554
555 #[test]
556 fn test_config_from_toml() {
557 let mut config = crate::config::Config::default();
558 let mut rule_config = crate::config::RuleConfig::default();
559 rule_config
560 .values
561 .insert("maximum".to_string(), toml::Value::Integer(3));
562 config.rules.insert("MD012".to_string(), rule_config);
563
564 let rule = MD012NoMultipleBlanks::from_config(&config);
565 let content = "Line 1\n\n\n\nLine 2"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
567 let result = rule.check(&ctx).unwrap();
568 assert!(result.is_empty()); }
570
571 #[test]
572 fn test_blank_lines_between_sections() {
573 let rule = MD012NoMultipleBlanks::default();
574 let content = "# Section 1\n\nContent\n\n\n# Section 2\n\nContent";
575 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
576 let result = rule.check(&ctx).unwrap();
577 assert_eq!(result.len(), 1);
578 assert_eq!(result[0].line, 5);
579 }
580
581 #[test]
582 fn test_fix_preserves_indented_code() {
583 let rule = MD012NoMultipleBlanks::default();
584 let content = "Text\n\n\n code\n \n more code\n\n\nText";
585 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
586 let fixed = rule.fix(&ctx).unwrap();
587 assert_eq!(fixed, "Text\n\n code\n\n more code\n\nText");
589 }
590
591 #[test]
592 fn test_edge_case_only_blanks() {
593 let rule = MD012NoMultipleBlanks::default();
594 let content = "\n\n\n";
595 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
596 let result = rule.check(&ctx).unwrap();
597 assert_eq!(result.len(), 1);
599 assert!(result[0].message.contains("at end of file"));
600 }
601
602 #[test]
605 fn test_blanks_after_fenced_code_block_mid_document() {
606 let rule = MD012NoMultipleBlanks::default();
608 let content = "## Input\n\n```javascript\ncode\n```\n\n\n## Error\n";
609 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
610 let result = rule.check(&ctx).unwrap();
611 assert_eq!(result.len(), 1, "Should detect blanks after code block");
613 assert_eq!(result[0].line, 7, "Warning should be on line 7 (second blank)");
614 assert!(result[0].message.contains("between content"));
615 }
616
617 #[test]
618 fn test_blanks_after_code_block_at_eof() {
619 let rule = MD012NoMultipleBlanks::default();
621 let content = "# Heading\n\n```\ncode\n```\n\n\n";
622 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
623 let result = rule.check(&ctx).unwrap();
624 assert_eq!(result.len(), 1, "Should detect trailing blanks after code block");
626 assert!(result[0].message.contains("at end of file"));
627 }
628
629 #[test]
630 fn test_single_blank_after_code_block_allowed() {
631 let rule = MD012NoMultipleBlanks::default();
633 let content = "## Input\n\n```\ncode\n```\n\n## Output\n";
634 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
635 let result = rule.check(&ctx).unwrap();
636 assert!(result.is_empty(), "Single blank after code block should be allowed");
637 }
638
639 #[test]
640 fn test_multiple_code_blocks_with_blanks() {
641 let rule = MD012NoMultipleBlanks::default();
643 let content = "```\ncode1\n```\n\n\n```\ncode2\n```\n\n\nEnd\n";
644 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
645 let result = rule.check(&ctx).unwrap();
646 assert_eq!(result.len(), 2, "Should detect blanks after both code blocks");
648 }
649
650 #[test]
651 fn test_whitespace_only_lines_after_code_block_at_eof() {
652 let rule = MD012NoMultipleBlanks::default();
655 let content = "```\ncode\n```\n \n \n";
656 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
657 let result = rule.check(&ctx).unwrap();
658 assert_eq!(result.len(), 1, "Should detect whitespace-only trailing blanks");
659 assert!(result[0].message.contains("at end of file"));
660 }
661
662 #[test]
665 fn test_warning_fix_removes_single_trailing_blank() {
666 let rule = MD012NoMultipleBlanks::default();
668 let content = "hello foobar hello.\n\n";
669 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
670 let warnings = rule.check(&ctx).unwrap();
671
672 assert_eq!(warnings.len(), 1);
673 assert!(warnings[0].fix.is_some(), "Warning should have a fix attached");
674
675 let fix = warnings[0].fix.as_ref().unwrap();
676 assert_eq!(fix.replacement, "", "Replacement should be empty");
678
679 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
681 assert_eq!(fixed, "hello foobar hello.\n", "Should end with single newline");
682 }
683
684 #[test]
685 fn test_warning_fix_removes_multiple_trailing_blanks() {
686 let rule = MD012NoMultipleBlanks::default();
687 let content = "content\n\n\n\n";
688 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
689 let warnings = rule.check(&ctx).unwrap();
690
691 assert_eq!(warnings.len(), 1);
692 assert!(warnings[0].fix.is_some());
693
694 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
695 assert_eq!(fixed, "content\n", "Should end with single newline");
696 }
697
698 #[test]
699 fn test_warning_fix_preserves_content_newline() {
700 let rule = MD012NoMultipleBlanks::default();
702 let content = "line1\nline2\n\n";
703 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
704 let warnings = rule.check(&ctx).unwrap();
705
706 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
707 assert_eq!(fixed, "line1\nline2\n", "Should preserve all content lines");
708 }
709
710 #[test]
711 fn test_warning_fix_mid_document_blanks() {
712 let rule = MD012NoMultipleBlanks::default();
714 let content = "# Heading\n\n\n\nParagraph\n";
716 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
717 let warnings = rule.check(&ctx).unwrap();
718
719 assert_eq!(warnings.len(), 2, "Should have 2 warnings for 2 extra blank lines");
721 assert!(warnings[0].fix.is_some());
722 assert!(warnings[1].fix.is_some());
723
724 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
725 assert_eq!(fixed, "# Heading\n\nParagraph\n", "Should reduce to single blank");
726 }
727
728 #[test]
731 fn test_blank_lines_in_quarto_callout() {
732 let rule = MD012NoMultipleBlanks::default();
734 let content = "# Heading\n\n::: {.callout-note}\nNote content\n\n\nMore content\n:::\n\nAfter";
735 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
736 let result = rule.check(&ctx).unwrap();
737 assert!(result.is_empty(), "Should not flag blanks inside Quarto callouts");
738 }
739
740 #[test]
741 fn test_blank_lines_in_quarto_div() {
742 let rule = MD012NoMultipleBlanks::default();
744 let content = "Text\n\n::: {.bordered}\nContent\n\n\nMore\n:::\n\nText";
745 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
746 let result = rule.check(&ctx).unwrap();
747 assert!(result.is_empty(), "Should not flag blanks inside Quarto divs");
748 }
749
750 #[test]
751 fn test_blank_lines_outside_quarto_div_flagged() {
752 let rule = MD012NoMultipleBlanks::default();
754 let content = "Text\n\n\n::: {.callout-note}\nNote\n:::\n\n\nMore";
755 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
756 let result = rule.check(&ctx).unwrap();
757 assert!(!result.is_empty(), "Should flag blanks outside Quarto divs");
758 }
759
760 #[test]
761 fn test_quarto_divs_ignored_in_standard_flavor() {
762 let rule = MD012NoMultipleBlanks::default();
764 let content = "::: {.callout-note}\nNote content\n\n\nMore content\n:::\n";
765 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
766 let result = rule.check(&ctx).unwrap();
767 assert!(!result.is_empty(), "Standard flavor should flag blanks in 'div'");
769 }
770}