1use crate::filtered_lines::FilteredLinesExt;
2use crate::lint_context::LintContext;
3use crate::lint_context::types::HeadingStyle;
4use crate::utils::LineIndex;
5use crate::utils::range_utils::calculate_line_range;
6use std::collections::HashSet;
7
8use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
9use crate::rule_config_serde::RuleConfig;
10
11mod md012_config;
12use md012_config::MD012Config;
13
14#[derive(Debug, Clone)]
19pub struct MD012NoMultipleBlanks {
20 config: MD012Config,
21 heading_blanks_above: usize,
24 heading_blanks_below: usize,
27}
28
29impl Default for MD012NoMultipleBlanks {
30 fn default() -> Self {
31 Self {
32 config: MD012Config::default(),
33 heading_blanks_above: 1,
34 heading_blanks_below: 1,
35 }
36 }
37}
38
39impl MD012NoMultipleBlanks {
40 pub fn new(maximum: usize) -> Self {
41 use crate::types::PositiveUsize;
42 Self {
43 config: MD012Config {
44 maximum: PositiveUsize::new(maximum).unwrap_or(PositiveUsize::from_const(1)),
45 },
46 heading_blanks_above: 1,
47 heading_blanks_below: 1,
48 }
49 }
50
51 pub const fn from_config_struct(config: MD012Config) -> Self {
52 Self {
53 config,
54 heading_blanks_above: 1,
55 heading_blanks_below: 1,
56 }
57 }
58
59 pub fn with_heading_limits(mut self, above: usize, below: usize) -> Self {
62 self.heading_blanks_above = above;
63 self.heading_blanks_below = below;
64 self
65 }
66
67 fn effective_max_above(&self) -> usize {
71 self.config.maximum.get().max(self.heading_blanks_above)
72 }
73
74 fn effective_max_below(&self) -> usize {
75 self.config.maximum.get().max(self.heading_blanks_below)
76 }
77
78 fn generate_excess_warnings(
80 &self,
81 blank_start: usize,
82 blank_count: usize,
83 effective_max: usize,
84 lines: &[&str],
85 lines_to_check: &HashSet<usize>,
86 line_index: &LineIndex,
87 ) -> Vec<LintWarning> {
88 let mut warnings = Vec::new();
89
90 let location = if blank_start == 0 {
91 "at start of file"
92 } else {
93 "between content"
94 };
95
96 for i in effective_max..blank_count {
97 let excess_line_num = blank_start + i;
98 if lines_to_check.contains(&excess_line_num) {
99 let excess_line = excess_line_num + 1;
100 let excess_line_content = lines.get(excess_line_num).unwrap_or(&"");
101 let (start_line, start_col, end_line, end_col) = calculate_line_range(excess_line, excess_line_content);
102 warnings.push(LintWarning {
103 rule_name: Some(self.name().to_string()),
104 severity: Severity::Warning,
105 message: format!("Multiple consecutive blank lines {location}"),
106 line: start_line,
107 column: start_col,
108 end_line,
109 end_column: end_col,
110 fix: Some(Fix::new(
111 {
112 let line_start = line_index.get_line_start_byte(excess_line).unwrap_or(0);
113 let line_end = line_index
114 .get_line_start_byte(excess_line + 1)
115 .unwrap_or(line_start + 1);
116 line_start..line_end
117 },
118 String::new(),
119 )),
120 });
121 }
122 }
123
124 warnings
125 }
126}
127
128fn is_heading_context(ctx: &LintContext, line_idx: usize) -> bool {
134 if ctx.lines.get(line_idx).is_some_and(|li| li.heading.is_some()) {
135 return true;
136 }
137 if line_idx > 0
139 && let Some(prev_info) = ctx.lines.get(line_idx - 1)
140 && let Some(ref heading) = prev_info.heading
141 && matches!(heading.style, HeadingStyle::Setext1 | HeadingStyle::Setext2)
142 {
143 return true;
144 }
145 false
146}
147
148fn max_heading_limit(
152 level_config: &crate::rules::md022_blanks_around_headings::md022_config::HeadingLevelConfig,
153) -> usize {
154 let mut max_val: usize = 0;
155 for level in 1..=6 {
156 match level_config.get_for_level(level).required_count() {
157 None => return usize::MAX, Some(count) => max_val = max_val.max(count),
159 }
160 }
161 max_val
162}
163
164impl Rule for MD012NoMultipleBlanks {
165 fn name(&self) -> &'static str {
166 "MD012"
167 }
168
169 fn description(&self) -> &'static str {
170 "Multiple consecutive blank lines"
171 }
172
173 fn category(&self) -> RuleCategory {
174 RuleCategory::Whitespace
175 }
176
177 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
178 let content = ctx.content;
179
180 if content.is_empty() {
182 return Ok(Vec::new());
183 }
184
185 let lines = ctx.raw_lines();
188 let has_potential_blanks = lines
189 .windows(2)
190 .any(|pair| pair[0].trim().is_empty() && pair[1].trim().is_empty());
191
192 let ends_with_multiple_newlines = content.ends_with("\n\n");
195
196 if !has_potential_blanks && !ends_with_multiple_newlines {
197 return Ok(Vec::new());
198 }
199
200 let line_index = &ctx.line_index;
201
202 let mut warnings = Vec::new();
203
204 let mut blank_count = 0;
206 let mut blank_start = 0;
207 let mut last_line_num: Option<usize> = None;
208 let mut prev_content_line_num: Option<usize> = None;
210
211 let mut lines_to_check: HashSet<usize> = HashSet::new();
213
214 for filtered_line in ctx
221 .filtered_lines()
222 .skip_front_matter()
223 .skip_code_blocks()
224 .skip_html_comments()
225 .skip_html_blocks()
226 .skip_quarto_divs()
227 .skip_math_blocks()
228 .skip_obsidian_comments()
229 .skip_pymdown_blocks()
230 .skip_jsx_expressions()
231 .skip_mdx_comments()
232 .skip_jsx_blocks()
233 .skip_esm_blocks()
234 {
235 let line_num = filtered_line.line_num - 1; let line = filtered_line.content;
237
238 if let Some(last) = last_line_num
241 && line_num > last + 1
242 {
243 let effective_max = if prev_content_line_num.is_some_and(|idx| is_heading_context(ctx, idx)) {
246 self.effective_max_below()
247 } else {
248 self.config.maximum.get()
249 };
250 if blank_count > effective_max {
251 warnings.extend(self.generate_excess_warnings(
252 blank_start,
253 blank_count,
254 effective_max,
255 lines,
256 &lines_to_check,
257 line_index,
258 ));
259 }
260 blank_count = 0;
261 lines_to_check.clear();
262 prev_content_line_num = None;
264 }
265 last_line_num = Some(line_num);
266
267 if line.trim().is_empty() {
268 if blank_count == 0 {
269 blank_start = line_num;
270 }
271 blank_count += 1;
272 if blank_count > self.config.maximum.get() {
274 lines_to_check.insert(line_num);
275 }
276 } else {
277 let heading_below = prev_content_line_num.is_some_and(|idx| is_heading_context(ctx, idx));
283 let heading_above = blank_start > 0 && is_heading_context(ctx, line_num);
284 let effective_max = if heading_below && heading_above {
285 self.effective_max_above().max(self.effective_max_below())
287 } else if heading_below {
288 self.effective_max_below()
289 } else if heading_above {
290 self.effective_max_above()
291 } else {
292 self.config.maximum.get()
293 };
294
295 if blank_count > effective_max {
296 warnings.extend(self.generate_excess_warnings(
297 blank_start,
298 blank_count,
299 effective_max,
300 lines,
301 &lines_to_check,
302 line_index,
303 ));
304 }
305 blank_count = 0;
306 lines_to_check.clear();
307 prev_content_line_num = Some(line_num);
308 }
309 }
310
311 let last_line_is_blank = lines.last().is_some_and(|l| l.trim().is_empty());
318
319 if blank_count > 0 && last_line_is_blank {
323 let location = "at end of file";
324
325 let report_line = lines.len();
327
328 let fix_start = line_index
331 .get_line_start_byte(report_line - blank_count + 1)
332 .unwrap_or(0);
333 let fix_end = content.len();
334
335 warnings.push(LintWarning {
337 rule_name: Some(self.name().to_string()),
338 severity: Severity::Warning,
339 message: format!("Multiple consecutive blank lines {location}"),
340 line: report_line,
341 column: 1,
342 end_line: report_line,
343 end_column: 1,
344 fix: Some(Fix::new(fix_start..fix_end, String::new())),
345 });
346 }
347
348 Ok(warnings)
349 }
350
351 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
352 if self.should_skip(ctx) {
353 return Ok(ctx.content.to_string());
354 }
355 let warnings = self.check(ctx)?;
356 if warnings.is_empty() {
357 return Ok(ctx.content.to_string());
358 }
359 let warnings =
360 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
361 crate::utils::fix_utils::apply_warning_fixes(ctx.content, &warnings)
362 .map_err(crate::rule::LintError::InvalidInput)
363 }
364
365 fn as_any(&self) -> &dyn std::any::Any {
366 self
367 }
368
369 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
370 ctx.content.is_empty() || !ctx.has_char('\n')
372 }
373
374 fn default_config_section(&self) -> Option<(String, toml::Value)> {
375 let default_config = MD012Config::default();
376 let json_value = serde_json::to_value(&default_config).ok()?;
377 let toml_value = crate::rule_config_serde::json_to_toml_value(&json_value)?;
378
379 if let toml::Value::Table(table) = toml_value {
380 if !table.is_empty() {
381 Some((MD012Config::RULE_NAME.to_string(), toml::Value::Table(table)))
382 } else {
383 None
384 }
385 } else {
386 None
387 }
388 }
389
390 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
391 where
392 Self: Sized,
393 {
394 use crate::rules::md022_blanks_around_headings::md022_config::MD022Config;
395
396 let rule_config = crate::rule_config_serde::load_rule_config::<MD012Config>(config);
397
398 let md022_disabled = config.global.disable.iter().any(|r| r == "MD022")
401 || config.global.extend_disable.iter().any(|r| r == "MD022");
402
403 let (heading_above, heading_below) = if md022_disabled {
404 (rule_config.maximum.get(), rule_config.maximum.get())
406 } else {
407 let md022_config = crate::rule_config_serde::load_rule_config::<MD022Config>(config);
408 (
409 max_heading_limit(&md022_config.lines_above),
410 max_heading_limit(&md022_config.lines_below),
411 )
412 };
413
414 Box::new(Self {
415 config: rule_config,
416 heading_blanks_above: heading_above,
417 heading_blanks_below: heading_below,
418 })
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::lint_context::LintContext;
426
427 #[test]
428 fn test_single_blank_line_allowed() {
429 let rule = MD012NoMultipleBlanks::default();
430 let content = "Line 1\n\nLine 2\n\nLine 3";
431 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
432 let result = rule.check(&ctx).unwrap();
433 assert!(result.is_empty());
434 }
435
436 #[test]
437 fn test_multiple_blank_lines_flagged() {
438 let rule = MD012NoMultipleBlanks::default();
439 let content = "Line 1\n\n\nLine 2\n\n\n\nLine 3";
440 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
441 let result = rule.check(&ctx).unwrap();
442 assert_eq!(result.len(), 3); assert_eq!(result[0].line, 3);
444 assert_eq!(result[1].line, 6);
445 assert_eq!(result[2].line, 7);
446 }
447
448 #[test]
449 fn test_custom_maximum() {
450 let rule = MD012NoMultipleBlanks::new(2);
451 let content = "Line 1\n\n\nLine 2\n\n\n\nLine 3";
452 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
453 let result = rule.check(&ctx).unwrap();
454 assert_eq!(result.len(), 1); assert_eq!(result[0].line, 7);
456 }
457
458 #[test]
459 fn test_fix_multiple_blank_lines() {
460 let rule = MD012NoMultipleBlanks::default();
461 let content = "Line 1\n\n\nLine 2\n\n\n\nLine 3";
462 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
463 let fixed = rule.fix(&ctx).unwrap();
464 assert_eq!(fixed, "Line 1\n\nLine 2\n\nLine 3");
465 }
466
467 #[test]
468 fn test_blank_lines_in_code_block() {
469 let rule = MD012NoMultipleBlanks::default();
470 let content = "Before\n\n```\ncode\n\n\n\nmore code\n```\n\nAfter";
471 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
472 let result = rule.check(&ctx).unwrap();
473 assert!(result.is_empty()); }
475
476 #[test]
477 fn test_blank_lines_in_html_comment() {
478 let rule = MD012NoMultipleBlanks::default();
479 let content = "Before\n\n<!--\ncomment\n\n\n\nmore comment\n-->\n\nAfter";
480 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
481 let result = rule.check(&ctx).unwrap();
482 assert!(result.is_empty()); }
484
485 #[test]
486 fn test_blank_lines_in_html_block() {
487 let rule = MD012NoMultipleBlanks::default();
488 let content = "Before\n\n<script>\n\n\n\n</script>\n\nAfter";
489 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
490 let result = rule.check(&ctx).unwrap();
491 assert!(result.is_empty()); }
493
494 #[test]
495 fn test_fix_preserves_code_block_blanks() {
496 let rule = MD012NoMultipleBlanks::default();
497 let content = "Before\n\n\n```\ncode\n\n\n\nmore code\n```\n\n\nAfter";
498 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
499 let fixed = rule.fix(&ctx).unwrap();
500 assert_eq!(fixed, "Before\n\n```\ncode\n\n\n\nmore code\n```\n\nAfter");
501 }
502
503 #[test]
504 fn test_blank_lines_in_front_matter() {
505 let rule = MD012NoMultipleBlanks::default();
506 let content = "---\ntitle: Test\n\n\nauthor: Me\n---\n\nContent";
507 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
508 let result = rule.check(&ctx).unwrap();
509 assert!(result.is_empty()); }
511
512 #[test]
513 fn test_blank_lines_at_start() {
514 let rule = MD012NoMultipleBlanks::default();
515 let content = "\n\n\nContent";
516 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
517 let result = rule.check(&ctx).unwrap();
518 assert_eq!(result.len(), 2);
519 assert!(result[0].message.contains("at start of file"));
520 }
521
522 #[test]
523 fn test_blank_lines_at_end() {
524 let rule = MD012NoMultipleBlanks::default();
525 let content = "Content\n\n\n";
526 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
527 let result = rule.check(&ctx).unwrap();
528 assert_eq!(result.len(), 1);
529 assert!(result[0].message.contains("at end of file"));
530 }
531
532 #[test]
533 fn test_single_blank_at_eof_flagged() {
534 let rule = MD012NoMultipleBlanks::default();
536 let content = "Content\n\n";
537 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
538 let result = rule.check(&ctx).unwrap();
539 assert_eq!(result.len(), 1);
540 assert!(result[0].message.contains("at end of file"));
541 }
542
543 #[test]
544 fn test_whitespace_only_lines() {
545 let rule = MD012NoMultipleBlanks::default();
546 let content = "Line 1\n \n\t\nLine 2";
547 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
548 let result = rule.check(&ctx).unwrap();
549 assert_eq!(result.len(), 1); }
551
552 #[test]
553 fn test_indented_code_blocks() {
554 let rule = MD012NoMultipleBlanks::default();
556 let content = "Text\n\n code\n \n \n more code\n\nText";
557 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
558 let result = rule.check(&ctx).unwrap();
559 assert!(result.is_empty(), "Should not flag blanks inside indented code blocks");
560 }
561
562 #[test]
563 fn test_blanks_in_indented_code_block() {
564 let content = " code line 1\n\n\n code line 2\n";
566 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
567 let rule = MD012NoMultipleBlanks::default();
568 let warnings = rule.check(&ctx).unwrap();
569 assert!(warnings.is_empty(), "Should not flag blanks in indented code");
570 }
571
572 #[test]
573 fn test_blanks_in_indented_code_block_with_heading() {
574 let content = "# Heading\n\n code line 1\n\n\n code line 2\n\nMore text\n";
576 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
577 let rule = MD012NoMultipleBlanks::default();
578 let warnings = rule.check(&ctx).unwrap();
579 assert!(
580 warnings.is_empty(),
581 "Should not flag blanks in indented code after heading"
582 );
583 }
584
585 #[test]
586 fn test_blanks_after_indented_code_block_flagged() {
587 let content = "# Heading\n\n code line\n\n\n\nMore text\n";
589 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
590 let rule = MD012NoMultipleBlanks::default();
591 let warnings = rule.check(&ctx).unwrap();
592 assert_eq!(warnings.len(), 2, "Should flag blanks after indented code block ends");
594 }
595
596 #[test]
597 fn test_fix_with_final_newline() {
598 let rule = MD012NoMultipleBlanks::default();
599 let content = "Line 1\n\n\nLine 2\n";
600 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
601 let fixed = rule.fix(&ctx).unwrap();
602 assert_eq!(fixed, "Line 1\n\nLine 2\n");
603 assert!(fixed.ends_with('\n'));
604 }
605
606 #[test]
607 fn test_empty_content() {
608 let rule = MD012NoMultipleBlanks::default();
609 let content = "";
610 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
611 let result = rule.check(&ctx).unwrap();
612 assert!(result.is_empty());
613 }
614
615 #[test]
616 fn test_nested_code_blocks() {
617 let rule = MD012NoMultipleBlanks::default();
618 let content = "Before\n\n~~~\nouter\n\n```\ninner\n\n\n```\n\n~~~\n\nAfter";
619 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
620 let result = rule.check(&ctx).unwrap();
621 assert!(result.is_empty());
622 }
623
624 #[test]
625 fn test_unclosed_code_block() {
626 let rule = MD012NoMultipleBlanks::default();
627 let content = "Before\n\n```\ncode\n\n\n\nno closing fence";
628 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
629 let result = rule.check(&ctx).unwrap();
630 assert!(result.is_empty()); }
632
633 #[test]
634 fn test_mixed_fence_styles() {
635 let rule = MD012NoMultipleBlanks::default();
636 let content = "Before\n\n```\ncode\n\n\n~~~\n\nAfter";
637 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
638 let result = rule.check(&ctx).unwrap();
639 assert!(result.is_empty()); }
641
642 #[test]
643 fn test_config_from_toml() {
644 let mut config = crate::config::Config::default();
645 let mut rule_config = crate::config::RuleConfig::default();
646 rule_config
647 .values
648 .insert("maximum".to_string(), toml::Value::Integer(3));
649 config.rules.insert("MD012".to_string(), rule_config);
650
651 let rule = MD012NoMultipleBlanks::from_config(&config);
652 let content = "Line 1\n\n\n\nLine 2"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
654 let result = rule.check(&ctx).unwrap();
655 assert!(result.is_empty()); }
657
658 #[test]
659 fn test_blank_lines_between_sections() {
660 let rule = MD012NoMultipleBlanks::default().with_heading_limits(2, 1);
662 let content = "# Section 1\n\nContent\n\n\n# Section 2\n\nContent";
663 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
664 let result = rule.check(&ctx).unwrap();
665 assert!(
666 result.is_empty(),
667 "2 blanks above heading allowed with heading_blanks_above=2"
668 );
669 }
670
671 #[test]
672 fn test_fix_preserves_indented_code() {
673 let rule = MD012NoMultipleBlanks::default();
674 let content = "Text\n\n\n code\n \n more code\n\n\nText";
675 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
676 let fixed = rule.fix(&ctx).unwrap();
677 assert_eq!(fixed, "Text\n\n code\n \n more code\n\nText");
680 }
681
682 #[test]
683 fn test_edge_case_only_blanks() {
684 let rule = MD012NoMultipleBlanks::default();
685 let content = "\n\n\n";
686 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
687 let result = rule.check(&ctx).unwrap();
688 assert_eq!(result.len(), 1);
690 assert!(result[0].message.contains("at end of file"));
691 }
692
693 #[test]
696 fn test_blanks_after_fenced_code_block_mid_document() {
697 let rule = MD012NoMultipleBlanks::default().with_heading_limits(2, 1);
699 let content = "## Input\n\n```javascript\ncode\n```\n\n\n## Error\n";
700 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
701 let result = rule.check(&ctx).unwrap();
702 assert!(
703 result.is_empty(),
704 "2 blanks before heading allowed with heading_blanks_above=2"
705 );
706 }
707
708 #[test]
709 fn test_blanks_after_code_block_at_eof() {
710 let rule = MD012NoMultipleBlanks::default();
712 let content = "# Heading\n\n```\ncode\n```\n\n\n";
713 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
714 let result = rule.check(&ctx).unwrap();
715 assert_eq!(result.len(), 1, "Should detect trailing blanks after code block");
717 assert!(result[0].message.contains("at end of file"));
718 }
719
720 #[test]
721 fn test_single_blank_after_code_block_allowed() {
722 let rule = MD012NoMultipleBlanks::default();
724 let content = "## Input\n\n```\ncode\n```\n\n## Output\n";
725 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
726 let result = rule.check(&ctx).unwrap();
727 assert!(result.is_empty(), "Single blank after code block should be allowed");
728 }
729
730 #[test]
731 fn test_multiple_code_blocks_with_blanks() {
732 let rule = MD012NoMultipleBlanks::default();
734 let content = "```\ncode1\n```\n\n\n```\ncode2\n```\n\n\nEnd\n";
735 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
736 let result = rule.check(&ctx).unwrap();
737 assert_eq!(result.len(), 2, "Should detect blanks after both code blocks");
739 }
740
741 #[test]
742 fn test_whitespace_only_lines_after_code_block_at_eof() {
743 let rule = MD012NoMultipleBlanks::default();
746 let content = "```\ncode\n```\n \n \n";
747 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
748 let result = rule.check(&ctx).unwrap();
749 assert_eq!(result.len(), 1, "Should detect whitespace-only trailing blanks");
750 assert!(result[0].message.contains("at end of file"));
751 }
752
753 #[test]
756 fn test_warning_fix_removes_single_trailing_blank() {
757 let rule = MD012NoMultipleBlanks::default();
759 let content = "hello foobar hello.\n\n";
760 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
761 let warnings = rule.check(&ctx).unwrap();
762
763 assert_eq!(warnings.len(), 1);
764 assert!(warnings[0].fix.is_some(), "Warning should have a fix attached");
765
766 let fix = warnings[0].fix.as_ref().unwrap();
767 assert_eq!(fix.replacement, "", "Replacement should be empty");
769
770 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
772 assert_eq!(fixed, "hello foobar hello.\n", "Should end with single newline");
773 }
774
775 #[test]
776 fn test_warning_fix_removes_multiple_trailing_blanks() {
777 let rule = MD012NoMultipleBlanks::default();
778 let content = "content\n\n\n\n";
779 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
780 let warnings = rule.check(&ctx).unwrap();
781
782 assert_eq!(warnings.len(), 1);
783 assert!(warnings[0].fix.is_some());
784
785 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
786 assert_eq!(fixed, "content\n", "Should end with single newline");
787 }
788
789 #[test]
790 fn test_warning_fix_preserves_content_newline() {
791 let rule = MD012NoMultipleBlanks::default();
793 let content = "line1\nline2\n\n";
794 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
795 let warnings = rule.check(&ctx).unwrap();
796
797 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
798 assert_eq!(fixed, "line1\nline2\n", "Should preserve all content lines");
799 }
800
801 #[test]
802 fn test_warning_fix_mid_document_blanks() {
803 let rule = MD012NoMultipleBlanks::default();
805 let content = "# Heading\n\n\n\nParagraph\n";
806 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
807 let warnings = rule.check(&ctx).unwrap();
808 assert_eq!(
809 warnings.len(),
810 2,
811 "Excess heading-adjacent blanks flagged with default limits"
812 );
813 }
814
815 #[test]
820 fn test_heading_aware_blanks_below_with_higher_limit() {
821 let rule = MD012NoMultipleBlanks::default().with_heading_limits(1, 2);
823 let content = "# Heading\n\n\nParagraph\n";
824 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
825 let result = rule.check(&ctx).unwrap();
826 assert!(
827 result.is_empty(),
828 "2 blanks below heading allowed with heading_blanks_below=2"
829 );
830 }
831
832 #[test]
833 fn test_heading_aware_blanks_above_with_higher_limit() {
834 let rule = MD012NoMultipleBlanks::default().with_heading_limits(2, 1);
836 let content = "Paragraph\n\n\n# Heading\n";
837 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
838 let result = rule.check(&ctx).unwrap();
839 assert!(
840 result.is_empty(),
841 "2 blanks above heading allowed with heading_blanks_above=2"
842 );
843 }
844
845 #[test]
846 fn test_heading_aware_blanks_between_headings() {
847 let rule = MD012NoMultipleBlanks::default().with_heading_limits(2, 2);
849 let content = "# Heading 1\n\n\n## Heading 2\n";
850 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
851 let result = rule.check(&ctx).unwrap();
852 assert!(result.is_empty(), "2 blanks between headings allowed with limits=2");
853 }
854
855 #[test]
856 fn test_heading_aware_excess_still_flagged() {
857 let rule = MD012NoMultipleBlanks::default().with_heading_limits(2, 2);
859 let content = "# Heading\n\n\n\n\nParagraph\n"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
861 let result = rule.check(&ctx).unwrap();
862 assert_eq!(result.len(), 2, "Excess beyond heading limit should be flagged");
863 }
864
865 #[test]
866 fn test_heading_aware_setext_blanks_below() {
867 let rule = MD012NoMultipleBlanks::default().with_heading_limits(1, 2);
869 let content = "Heading\n=======\n\n\nParagraph\n";
870 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
871 let result = rule.check(&ctx).unwrap();
872 assert!(result.is_empty(), "2 blanks below Setext heading allowed with limit=2");
873 }
874
875 #[test]
876 fn test_heading_aware_setext_blanks_above() {
877 let rule = MD012NoMultipleBlanks::default().with_heading_limits(2, 1);
879 let content = "Paragraph\n\n\nHeading\n=======\n";
880 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
881 let result = rule.check(&ctx).unwrap();
882 assert!(result.is_empty(), "2 blanks above Setext heading allowed with limit=2");
883 }
884
885 #[test]
886 fn test_heading_aware_single_blank_allowed() {
887 let rule = MD012NoMultipleBlanks::default();
889 let content = "# Heading\n\nParagraph\n";
890 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
891 let result = rule.check(&ctx).unwrap();
892 assert!(result.is_empty(), "Single blank near heading should be allowed");
893 }
894
895 #[test]
896 fn test_heading_aware_non_heading_blanks_still_flagged() {
897 let rule = MD012NoMultipleBlanks::default();
899 let content = "Paragraph 1\n\n\nParagraph 2\n";
900 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
901 let result = rule.check(&ctx).unwrap();
902 assert_eq!(result.len(), 1, "Non-heading blanks should still be flagged");
903 }
904
905 #[test]
906 fn test_heading_aware_fix_caps_heading_blanks() {
907 let rule = MD012NoMultipleBlanks::default().with_heading_limits(1, 2);
909 let content = "# Heading\n\n\n\nParagraph\n"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
911 let fixed = rule.fix(&ctx).unwrap();
912 assert_eq!(
913 fixed, "# Heading\n\n\nParagraph\n",
914 "Fix caps heading-adjacent blanks at effective max (2)"
915 );
916 }
917
918 #[test]
919 fn test_heading_aware_fix_preserves_allowed_heading_blanks() {
920 let rule = MD012NoMultipleBlanks::default().with_heading_limits(1, 3);
922 let content = "# Heading\n\n\n\nParagraph\n"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
924 let fixed = rule.fix(&ctx).unwrap();
925 assert_eq!(
926 fixed, "# Heading\n\n\n\nParagraph\n",
927 "Fix preserves blanks within the heading limit"
928 );
929 }
930
931 #[test]
932 fn test_heading_aware_fix_reduces_non_heading_blanks() {
933 let rule = MD012NoMultipleBlanks::default();
935 let content = "Paragraph 1\n\n\n\nParagraph 2\n";
936 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
937 let fixed = rule.fix(&ctx).unwrap();
938 assert_eq!(
939 fixed, "Paragraph 1\n\nParagraph 2\n",
940 "Fix should reduce non-heading blanks"
941 );
942 }
943
944 #[test]
945 fn test_heading_aware_mixed_heading_and_non_heading() {
946 let rule = MD012NoMultipleBlanks::default().with_heading_limits(1, 2);
948 let content = "# Heading\n\n\nParagraph 1\n\n\nParagraph 2\n";
949 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
950 let result = rule.check(&ctx).unwrap();
951 assert_eq!(result.len(), 1, "Only non-heading excess should be flagged");
953 }
954
955 #[test]
956 fn test_heading_aware_blanks_at_start_before_heading_still_flagged() {
957 let rule = MD012NoMultipleBlanks::default().with_heading_limits(3, 3);
960 let content = "\n\n\n# Heading\n";
961 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
962 let result = rule.check(&ctx).unwrap();
963 assert_eq!(
964 result.len(),
965 2,
966 "Start-of-file blanks should be flagged even before heading"
967 );
968 assert!(result[0].message.contains("at start of file"));
969 }
970
971 #[test]
972 fn test_heading_aware_eof_blanks_after_heading_still_flagged() {
973 let rule = MD012NoMultipleBlanks::default();
975 let content = "# Heading\n\n";
976 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
977 let result = rule.check(&ctx).unwrap();
978 assert_eq!(result.len(), 1, "EOF blanks should still be flagged");
979 assert!(result[0].message.contains("at end of file"));
980 }
981
982 #[test]
983 fn test_heading_aware_unlimited_heading_blanks() {
984 let rule = MD012NoMultipleBlanks::default().with_heading_limits(usize::MAX, usize::MAX);
986 let content = "# Heading\n\n\n\n\nParagraph\n"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
988 let result = rule.check(&ctx).unwrap();
989 assert!(
990 result.is_empty(),
991 "Unlimited heading limits means MD012 never flags near headings"
992 );
993 }
994
995 #[test]
996 fn test_heading_aware_blanks_after_code_then_heading() {
997 let rule = MD012NoMultipleBlanks::default().with_heading_limits(2, 2);
999 let content = "# Heading\n\n```\ncode\n```\n\n\n\nMore text\n";
1000 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1001 let result = rule.check(&ctx).unwrap();
1002 assert_eq!(result.len(), 2, "Non-heading blanks after code block should be flagged");
1004 }
1005
1006 #[test]
1007 fn test_heading_aware_fix_mixed_document() {
1008 let rule = MD012NoMultipleBlanks::default().with_heading_limits(2, 2);
1010 let content = "# Title\n\n\n## Section\n\n\nPara 1\n\n\nPara 2\n";
1011 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1012 let fixed = rule.fix(&ctx).unwrap();
1013 assert_eq!(fixed, "# Title\n\n\n## Section\n\n\nPara 1\n\nPara 2\n");
1015 }
1016
1017 #[test]
1018 fn test_heading_aware_from_config_reads_md022() {
1019 let mut config = crate::config::Config::default();
1021 let mut md022_config = crate::config::RuleConfig::default();
1022 md022_config
1023 .values
1024 .insert("lines-above".to_string(), toml::Value::Integer(2));
1025 md022_config
1026 .values
1027 .insert("lines-below".to_string(), toml::Value::Integer(3));
1028 config.rules.insert("MD022".to_string(), md022_config);
1029
1030 let rule = MD012NoMultipleBlanks::from_config(&config);
1031 let content = "Paragraph\n\n\n# Heading\n";
1033 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1034 let result = rule.check(&ctx).unwrap();
1035 assert!(
1036 result.is_empty(),
1037 "2 blanks above heading allowed when MD022 lines-above=2"
1038 );
1039 }
1040
1041 #[test]
1042 fn test_heading_aware_from_config_md022_disabled() {
1043 let mut config = crate::config::Config::default();
1045 config.global.disable.push("MD022".to_string());
1046
1047 let mut md022_config = crate::config::RuleConfig::default();
1048 md022_config
1049 .values
1050 .insert("lines-above".to_string(), toml::Value::Integer(3));
1051 config.rules.insert("MD022".to_string(), md022_config);
1052
1053 let rule = MD012NoMultipleBlanks::from_config(&config);
1054 let content = "Paragraph\n\n\n# Heading\n";
1056 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1057 let result = rule.check(&ctx).unwrap();
1058 assert_eq!(
1059 result.len(),
1060 1,
1061 "With MD022 disabled, heading-adjacent blanks are flagged"
1062 );
1063 }
1064
1065 #[test]
1066 fn test_heading_aware_from_config_md022_unlimited() {
1067 let mut config = crate::config::Config::default();
1069 let mut md022_config = crate::config::RuleConfig::default();
1070 md022_config
1071 .values
1072 .insert("lines-above".to_string(), toml::Value::Integer(-1));
1073 config.rules.insert("MD022".to_string(), md022_config);
1074
1075 let rule = MD012NoMultipleBlanks::from_config(&config);
1076 let content = "Paragraph\n\n\n\n\n# Heading\n"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1078 let result = rule.check(&ctx).unwrap();
1079 assert!(
1080 result.is_empty(),
1081 "Unlimited MD022 lines-above means MD012 never flags above headings"
1082 );
1083 }
1084
1085 #[test]
1086 fn test_heading_aware_from_config_per_level() {
1087 let mut config = crate::config::Config::default();
1092 let mut md022_config = crate::config::RuleConfig::default();
1093 md022_config.values.insert(
1094 "lines-above".to_string(),
1095 toml::Value::Array(vec![
1096 toml::Value::Integer(2),
1097 toml::Value::Integer(1),
1098 toml::Value::Integer(1),
1099 toml::Value::Integer(1),
1100 toml::Value::Integer(1),
1101 toml::Value::Integer(1),
1102 ]),
1103 );
1104 config.rules.insert("MD022".to_string(), md022_config);
1105
1106 let rule = MD012NoMultipleBlanks::from_config(&config);
1107
1108 let content = "Paragraph\n\n\n## H2 Heading\n";
1110 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1111 let result = rule.check(&ctx).unwrap();
1112 assert!(result.is_empty(), "Per-level max (2) allows 2 blanks above any heading");
1113
1114 let content = "Paragraph\n\n\n\n## H2 Heading\n";
1116 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1117 let result = rule.check(&ctx).unwrap();
1118 assert_eq!(result.len(), 1, "3 blanks exceeds per-level max of 2");
1119 }
1120
1121 #[test]
1122 fn test_issue_449_reproduction() {
1123 let rule = MD012NoMultipleBlanks::default();
1126 let content = "\
1127# Heading
1128
1129
1130Some introductory text.
1131
1132
1133
1134
1135
1136## Heading level 2
1137
1138
1139Some text for this section.
1140
1141Some more text for this section.
1142
1143
1144## Another heading level 2
1145
1146
1147
1148Some text for this section.
1149
1150Some more text for this section.
1151";
1152 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1153 let result = rule.check(&ctx).unwrap();
1154 assert!(
1155 !result.is_empty(),
1156 "Issue #449: excess blanks around headings should be flagged with default settings"
1157 );
1158
1159 let fixed = rule.fix(&ctx).unwrap();
1161 let fixed_ctx = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
1162 let recheck = rule.check(&fixed_ctx).unwrap();
1163 assert!(recheck.is_empty(), "Fix should resolve all excess blank lines");
1164
1165 assert!(fixed.contains("# Heading\n\nSome"), "1 blank below first heading");
1167 assert!(
1168 fixed.contains("text.\n\n## Heading level 2"),
1169 "1 blank above second heading"
1170 );
1171 }
1172
1173 #[test]
1176 fn test_blank_lines_in_quarto_callout() {
1177 let rule = MD012NoMultipleBlanks::default();
1179 let content = "# Heading\n\n::: {.callout-note}\nNote content\n\n\nMore content\n:::\n\nAfter";
1180 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
1181 let result = rule.check(&ctx).unwrap();
1182 assert!(result.is_empty(), "Should not flag blanks inside Quarto callouts");
1183 }
1184
1185 #[test]
1186 fn test_blank_lines_in_quarto_div() {
1187 let rule = MD012NoMultipleBlanks::default();
1189 let content = "Text\n\n::: {.bordered}\nContent\n\n\nMore\n:::\n\nText";
1190 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
1191 let result = rule.check(&ctx).unwrap();
1192 assert!(result.is_empty(), "Should not flag blanks inside Quarto divs");
1193 }
1194
1195 #[test]
1196 fn test_blank_lines_outside_quarto_div_flagged() {
1197 let rule = MD012NoMultipleBlanks::default();
1199 let content = "Text\n\n\n::: {.callout-note}\nNote\n:::\n\n\nMore";
1200 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
1201 let result = rule.check(&ctx).unwrap();
1202 assert!(!result.is_empty(), "Should flag blanks outside Quarto divs");
1203 }
1204
1205 #[test]
1206 fn test_quarto_divs_ignored_in_standard_flavor() {
1207 let rule = MD012NoMultipleBlanks::default();
1209 let content = "::: {.callout-note}\nNote content\n\n\nMore content\n:::\n";
1210 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1211 let result = rule.check(&ctx).unwrap();
1212 assert!(!result.is_empty(), "Standard flavor should flag blanks in 'div'");
1214 }
1215
1216 #[test]
1219 fn test_roundtrip_multiple_blank_lines() {
1220 let rule = MD012NoMultipleBlanks::default();
1221 let content = "Line 1\n\n\nLine 2\n\n\n\nLine 3";
1222 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1223 let fixed = rule.fix(&ctx).unwrap();
1224 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
1225 let recheck = rule.check(&ctx2).unwrap();
1226 assert!(
1227 recheck.is_empty(),
1228 "Roundtrip: fix then check should be clean, got {recheck:?}"
1229 );
1230 }
1231
1232 #[test]
1233 fn test_roundtrip_trailing_blanks() {
1234 let rule = MD012NoMultipleBlanks::default();
1235 let content = "Content\n\n\n\n";
1236 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1237 let fixed = rule.fix(&ctx).unwrap();
1238 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
1239 let recheck = rule.check(&ctx2).unwrap();
1240 assert!(recheck.is_empty(), "Roundtrip: trailing blanks, got {recheck:?}");
1241 }
1242
1243 #[test]
1244 fn test_roundtrip_leading_blanks() {
1245 let rule = MD012NoMultipleBlanks::default();
1246 let content = "\n\n\nContent\n";
1247 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1248 let fixed = rule.fix(&ctx).unwrap();
1249 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
1250 let recheck = rule.check(&ctx2).unwrap();
1251 assert!(recheck.is_empty(), "Roundtrip: leading blanks, got {recheck:?}");
1252 }
1253
1254 #[test]
1255 fn test_roundtrip_custom_maximum() {
1256 let rule = MD012NoMultipleBlanks::new(2);
1257 let content = "Line 1\n\n\n\n\nLine 2\n";
1258 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1259 let fixed = rule.fix(&ctx).unwrap();
1260 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
1261 let recheck = rule.check(&ctx2).unwrap();
1262 assert!(recheck.is_empty(), "Roundtrip: max=2, got {recheck:?}");
1263 }
1264
1265 #[test]
1266 fn test_roundtrip_code_blocks() {
1267 let rule = MD012NoMultipleBlanks::default();
1268 let content = "Before\n\n\n```\ncode\n\n\n\nmore code\n```\n\n\nAfter";
1269 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1270 let fixed = rule.fix(&ctx).unwrap();
1271 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
1272 let recheck = rule.check(&ctx2).unwrap();
1273 assert!(recheck.is_empty(), "Roundtrip: code blocks, got {recheck:?}");
1274 }
1275
1276 #[test]
1277 fn test_roundtrip_heading_limits() {
1278 let rule = MD012NoMultipleBlanks::default().with_heading_limits(2, 2);
1279 let content = "# Heading\n\n\n\n\nParagraph\n\n\n\n## Heading 2\n";
1280 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1281 let fixed = rule.fix(&ctx).unwrap();
1282 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
1283 let recheck = rule.check(&ctx2).unwrap();
1284 assert!(recheck.is_empty(), "Roundtrip: heading limits, got {recheck:?}");
1285 }
1286
1287 #[test]
1288 fn test_roundtrip_front_matter() {
1289 let rule = MD012NoMultipleBlanks::default();
1290 let content = "---\ntitle: Test\n\n\nauthor: Me\n---\n\n\n\nContent\n";
1291 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1292 let fixed = rule.fix(&ctx).unwrap();
1293 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
1294 let recheck = rule.check(&ctx2).unwrap();
1295 assert!(recheck.is_empty(), "Roundtrip: front matter, got {recheck:?}");
1296 }
1297
1298 #[test]
1299 fn test_roundtrip_only_blanks() {
1300 let rule = MD012NoMultipleBlanks::default();
1301 let content = "\n\n\n";
1302 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1303 let fixed = rule.fix(&ctx).unwrap();
1304 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
1305 let recheck = rule.check(&ctx2).unwrap();
1306 assert!(recheck.is_empty(), "Roundtrip: only blanks, got {recheck:?}");
1307 }
1308
1309 #[test]
1310 fn test_roundtrip_single_eof_blank() {
1311 let rule = MD012NoMultipleBlanks::default();
1312 let content = "Content\n\n";
1313 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1314 let fixed = rule.fix(&ctx).unwrap();
1315 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
1316 let recheck = rule.check(&ctx2).unwrap();
1317 assert!(recheck.is_empty(), "Roundtrip: single EOF blank, got {recheck:?}");
1318 }
1319
1320 #[test]
1321 fn test_roundtrip_mixed_heading_and_non_heading() {
1322 let rule = MD012NoMultipleBlanks::default().with_heading_limits(1, 2);
1323 let content = "# Heading\n\n\n\nParagraph 1\n\n\n\nParagraph 2\n";
1324 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1325 let fixed = rule.fix(&ctx).unwrap();
1326 let ctx2 = LintContext::new(&fixed, crate::config::MarkdownFlavor::Standard, None);
1327 let recheck = rule.check(&ctx2).unwrap();
1328 assert!(
1329 recheck.is_empty(),
1330 "Roundtrip: mixed heading/non-heading, got {recheck:?}"
1331 );
1332 }
1333}