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