1use crate::lint_context::LintContext;
2use crate::rule::{LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
3use crate::utils::skip_context::is_table_line;
4
5#[derive(Debug, Clone, PartialEq, Eq, Default)]
24pub enum ListItemSpacingStyle {
25 #[default]
26 Consistent,
27 Loose,
28 Tight,
29}
30
31#[derive(Debug, Clone, Default)]
32pub(super) struct MD076Config {
33 pub style: ListItemSpacingStyle,
34 pub allow_loose_continuation: bool,
38}
39
40#[derive(Debug, Clone, Default)]
41pub struct MD076ListItemSpacing {
42 config: MD076Config,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47enum GapKind {
48 Tight,
50 Loose,
52 Structural,
55 ContinuationLoose,
59}
60
61struct BlockAnalysis {
63 items: Vec<usize>,
65 gaps: Vec<GapKind>,
67 warn_loose_gaps: bool,
69 warn_tight_gaps: bool,
71}
72
73impl MD076ListItemSpacing {
74 pub fn new(style: ListItemSpacingStyle) -> Self {
75 Self {
76 config: MD076Config {
77 style,
78 allow_loose_continuation: false,
79 },
80 }
81 }
82
83 pub fn with_allow_loose_continuation(mut self, allow: bool) -> Self {
84 self.config.allow_loose_continuation = allow;
85 self
86 }
87
88 fn is_effectively_blank(ctx: &LintContext, line_num: usize) -> bool {
93 if let Some(info) = ctx.line_info(line_num) {
94 let content = info.content(ctx.content);
95 if content.trim().is_empty() {
96 return true;
97 }
98 if let Some(ref bq) = info.blockquote {
100 return bq.content.trim().is_empty();
101 }
102 false
103 } else {
104 false
105 }
106 }
107
108 fn is_structural_content(ctx: &LintContext, line_num: usize) -> bool {
111 if let Some(info) = ctx.line_info(line_num) {
112 if info.in_code_block {
114 return true;
115 }
116 if info.in_html_block {
118 return true;
119 }
120 if info.blockquote.is_some() {
122 return true;
123 }
124 let content = info.content(ctx.content);
126 let effective = if let Some(ref bq) = info.blockquote {
128 bq.content.as_str()
129 } else {
130 content
131 };
132 if is_table_line(effective.trim_start()) {
133 return true;
134 }
135 }
136 false
137 }
138
139 fn is_fenced_code_block_list_item(ctx: &LintContext, line_num: usize) -> bool {
150 let Some(info) = ctx.line_info(line_num) else {
151 return false;
152 };
153 if info.list_item.is_none() {
154 return false;
155 }
156
157 let line_range = info.byte_offset..info.byte_offset + info.byte_len;
158 ctx.code_block_details
159 .iter()
160 .any(|detail| detail.is_fenced && line_range.contains(&detail.start))
161 }
162
163 fn is_continuation_content(ctx: &LintContext, line_num: usize, parent_content_col: usize) -> bool {
170 let Some(info) = ctx.line_info(line_num) else {
171 return false;
172 };
173 if info.list_item.is_some() {
175 return false;
176 }
177 if info.in_code_block
179 || info.in_html_block
180 || info.in_html_comment
181 || info.in_mdx_comment
182 || info.in_front_matter
183 || info.in_math_block
184 || info.blockquote.is_some()
185 {
186 return false;
187 }
188 let content = info.content(ctx.content);
189 if content.trim().is_empty() {
190 return false;
191 }
192 let indent = content.len() - content.trim_start().len();
194 indent >= parent_content_col
195 }
196
197 fn classify_gap(ctx: &LintContext, first: usize, next: usize) -> GapKind {
205 if next <= first + 1 {
206 return GapKind::Tight;
207 }
208 if !Self::is_effectively_blank(ctx, next - 1) {
210 return GapKind::Tight;
211 }
212 if Self::is_fenced_code_block_list_item(ctx, next) {
216 return GapKind::Structural;
217 }
218 let mut scan = next - 1;
221 while scan > first && Self::is_effectively_blank(ctx, scan) {
222 scan -= 1;
223 }
224 if scan > first && Self::is_structural_content(ctx, scan) {
226 return GapKind::Structural;
227 }
228 let parent_content_col = ctx
231 .line_info(first)
232 .and_then(|li| li.list_item.as_ref())
233 .map_or(2, |item| item.content_column);
234 if scan > first && Self::is_continuation_content(ctx, scan, parent_content_col) {
235 return GapKind::ContinuationLoose;
236 }
237 GapKind::Loose
238 }
239
240 fn inter_item_blanks(ctx: &LintContext, first: usize, next: usize) -> Vec<usize> {
247 let mut blanks = Vec::new();
248 let mut line_num = next - 1;
249 while line_num > first && Self::is_effectively_blank(ctx, line_num) {
250 blanks.push(line_num);
251 line_num -= 1;
252 }
253 if line_num > first && Self::is_structural_content(ctx, line_num) {
255 return Vec::new();
256 }
257 blanks.reverse();
258 blanks
259 }
260
261 fn analyze_block(
266 ctx: &LintContext,
267 block: &crate::lint_context::types::ListBlock,
268 style: &ListItemSpacingStyle,
269 allow_loose_continuation: bool,
270 ) -> Option<BlockAnalysis> {
271 let items: Vec<usize> = block
275 .item_lines
276 .iter()
277 .copied()
278 .filter(|&line_num| {
279 ctx.line_info(line_num)
280 .and_then(|li| li.list_item.as_ref())
281 .is_some_and(|item| item.marker_column / 2 == block.nesting_level)
282 })
283 .collect();
284
285 if items.len() < 2 {
286 return None;
287 }
288
289 let gaps: Vec<GapKind> = items.windows(2).map(|w| Self::classify_gap(ctx, w[0], w[1])).collect();
291
292 let loose_count = gaps
296 .iter()
297 .filter(|&&g| g == GapKind::Loose || (g == GapKind::ContinuationLoose && !allow_loose_continuation))
298 .count();
299 let tight_count = gaps.iter().filter(|&&g| g == GapKind::Tight).count();
300
301 let (warn_loose_gaps, warn_tight_gaps) = match style {
302 ListItemSpacingStyle::Loose => (false, true),
303 ListItemSpacingStyle::Tight => (true, false),
304 ListItemSpacingStyle::Consistent => {
305 if loose_count == 0 || tight_count == 0 {
306 return None; }
308 if tight_count >= loose_count {
316 (true, false)
317 } else {
318 (false, true)
319 }
320 }
321 };
322
323 Some(BlockAnalysis {
324 items,
325 gaps,
326 warn_loose_gaps,
327 warn_tight_gaps,
328 })
329 }
330}
331
332impl Rule for MD076ListItemSpacing {
333 fn name(&self) -> &'static str {
334 "MD076"
335 }
336
337 fn description(&self) -> &'static str {
338 "List item spacing should be consistent"
339 }
340
341 fn category(&self) -> RuleCategory {
342 RuleCategory::List
343 }
344
345 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
346 ctx.content.is_empty() || ctx.list_blocks.is_empty()
347 }
348
349 fn check(&self, ctx: &LintContext) -> LintResult {
350 if ctx.content.is_empty() {
351 return Ok(Vec::new());
352 }
353
354 let mut warnings = Vec::new();
355
356 let allow_cont = self.config.allow_loose_continuation;
357
358 for block in &ctx.list_blocks {
359 let Some(analysis) = Self::analyze_block(ctx, block, &self.config.style, allow_cont) else {
360 continue;
361 };
362
363 for (i, &gap) in analysis.gaps.iter().enumerate() {
364 let is_loose_violation = match gap {
365 GapKind::Loose => analysis.warn_loose_gaps,
366 GapKind::ContinuationLoose => !allow_cont && analysis.warn_loose_gaps,
367 _ => false,
368 };
369
370 if is_loose_violation {
371 let blanks = Self::inter_item_blanks(ctx, analysis.items[i], analysis.items[i + 1]);
372 if let Some(&blank_line) = blanks.first() {
373 let line_content = ctx.line_info(blank_line).map_or("", |li| li.content(ctx.content));
374 warnings.push(LintWarning {
375 rule_name: Some(self.name().to_string()),
376 line: blank_line,
377 column: 1,
378 end_line: blank_line,
379 end_column: line_content.chars().count() + 1,
380 message: "Unexpected blank line between list items".to_string(),
381 severity: Severity::Warning,
382 fix: None,
383 });
384 }
385 } else if gap == GapKind::Tight && analysis.warn_tight_gaps {
386 let next_item = analysis.items[i + 1];
387 let line_content = ctx.line_info(next_item).map_or("", |li| li.content(ctx.content));
388 warnings.push(LintWarning {
389 rule_name: Some(self.name().to_string()),
390 line: next_item,
391 column: 1,
392 end_line: next_item,
393 end_column: line_content.chars().count() + 1,
394 message: "Missing blank line between list items".to_string(),
395 severity: Severity::Warning,
396 fix: None,
397 });
398 }
399 }
400 }
401
402 Ok(warnings)
403 }
404
405 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
406 if ctx.content.is_empty() {
407 return Ok(ctx.content.to_string());
408 }
409
410 let mut insert_before: std::collections::HashSet<usize> = std::collections::HashSet::new();
412 let mut remove_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
413
414 let allow_cont = self.config.allow_loose_continuation;
415
416 for block in &ctx.list_blocks {
417 let Some(analysis) = Self::analyze_block(ctx, block, &self.config.style, allow_cont) else {
418 continue;
419 };
420
421 for (i, &gap) in analysis.gaps.iter().enumerate() {
422 let is_loose_violation = match gap {
423 GapKind::Loose => analysis.warn_loose_gaps,
424 GapKind::ContinuationLoose => !allow_cont && analysis.warn_loose_gaps,
425 _ => false,
426 };
427
428 if is_loose_violation {
429 for blank_line in Self::inter_item_blanks(ctx, analysis.items[i], analysis.items[i + 1]) {
430 remove_lines.insert(blank_line);
431 }
432 } else if gap == GapKind::Tight && analysis.warn_tight_gaps {
433 insert_before.insert(analysis.items[i + 1]);
434 }
435 }
436 }
437
438 if insert_before.is_empty() && remove_lines.is_empty() {
439 return Ok(ctx.content.to_string());
440 }
441
442 let lines = ctx.raw_lines();
443 let mut result: Vec<String> = Vec::with_capacity(lines.len());
444
445 for (i, line) in lines.iter().enumerate() {
446 let line_num = i + 1;
447
448 if ctx.is_rule_disabled(self.name(), line_num) {
450 result.push((*line).to_string());
451 continue;
452 }
453
454 if remove_lines.contains(&line_num) {
455 continue;
456 }
457
458 if insert_before.contains(&line_num) {
459 let bq_prefix = ctx.blockquote_prefix_for_blank_line(i);
460 result.push(bq_prefix);
461 }
462
463 result.push((*line).to_string());
464 }
465
466 let mut output = result.join("\n");
467 if ctx.content.ends_with('\n') {
468 output.push('\n');
469 }
470 Ok(output)
471 }
472
473 fn as_any(&self) -> &dyn std::any::Any {
474 self
475 }
476
477 fn default_config_section(&self) -> Option<(String, toml::Value)> {
478 let mut map = toml::map::Map::new();
479 let style_str = match self.config.style {
480 ListItemSpacingStyle::Consistent => "consistent",
481 ListItemSpacingStyle::Loose => "loose",
482 ListItemSpacingStyle::Tight => "tight",
483 };
484 map.insert("style".to_string(), toml::Value::String(style_str.to_string()));
485 map.insert(
486 "allow-loose-continuation".to_string(),
487 toml::Value::Boolean(self.config.allow_loose_continuation),
488 );
489 Some((self.name().to_string(), toml::Value::Table(map)))
490 }
491
492 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
493 where
494 Self: Sized,
495 {
496 let style = crate::config::get_rule_config_value::<String>(config, "MD076", "style")
497 .unwrap_or_else(|| "consistent".to_string());
498 let style = match style.as_str() {
499 "loose" => ListItemSpacingStyle::Loose,
500 "tight" => ListItemSpacingStyle::Tight,
501 _ => ListItemSpacingStyle::Consistent,
502 };
503 let allow_loose_continuation =
504 crate::config::get_rule_config_value::<bool>(config, "MD076", "allow-loose-continuation")
505 .or_else(|| crate::config::get_rule_config_value::<bool>(config, "MD076", "allow_loose_continuation"))
506 .unwrap_or(false);
507 Box::new(Self::new(style).with_allow_loose_continuation(allow_loose_continuation))
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 fn check(content: &str, style: ListItemSpacingStyle) -> Vec<LintWarning> {
516 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
517 let rule = MD076ListItemSpacing::new(style);
518 rule.check(&ctx).unwrap()
519 }
520
521 fn check_with_continuation(
522 content: &str,
523 style: ListItemSpacingStyle,
524 allow_loose_continuation: bool,
525 ) -> Vec<LintWarning> {
526 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
527 let rule = MD076ListItemSpacing::new(style).with_allow_loose_continuation(allow_loose_continuation);
528 rule.check(&ctx).unwrap()
529 }
530
531 fn fix(content: &str, style: ListItemSpacingStyle) -> String {
532 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
533 let rule = MD076ListItemSpacing::new(style);
534 rule.fix(&ctx).unwrap()
535 }
536
537 fn fix_with_continuation(content: &str, style: ListItemSpacingStyle, allow_loose_continuation: bool) -> String {
538 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
539 let rule = MD076ListItemSpacing::new(style).with_allow_loose_continuation(allow_loose_continuation);
540 rule.fix(&ctx).unwrap()
541 }
542
543 #[test]
546 fn tight_list_tight_style_no_warnings() {
547 let content = "- Item 1\n- Item 2\n- Item 3\n";
548 assert!(check(content, ListItemSpacingStyle::Tight).is_empty());
549 }
550
551 #[test]
552 fn loose_list_loose_style_no_warnings() {
553 let content = "- Item 1\n\n- Item 2\n\n- Item 3\n";
554 assert!(check(content, ListItemSpacingStyle::Loose).is_empty());
555 }
556
557 #[test]
558 fn tight_list_loose_style_warns() {
559 let content = "- Item 1\n- Item 2\n- Item 3\n";
560 let warnings = check(content, ListItemSpacingStyle::Loose);
561 assert_eq!(warnings.len(), 2);
562 assert!(warnings.iter().all(|w| w.message.contains("Missing")));
563 }
564
565 #[test]
566 fn loose_list_tight_style_warns() {
567 let content = "- Item 1\n\n- Item 2\n\n- Item 3\n";
568 let warnings = check(content, ListItemSpacingStyle::Tight);
569 assert_eq!(warnings.len(), 2);
570 assert!(warnings.iter().all(|w| w.message.contains("Unexpected")));
571 }
572
573 #[test]
576 fn consistent_all_tight_no_warnings() {
577 let content = "- Item 1\n- Item 2\n- Item 3\n";
578 assert!(check(content, ListItemSpacingStyle::Consistent).is_empty());
579 }
580
581 #[test]
582 fn consistent_all_loose_no_warnings() {
583 let content = "- Item 1\n\n- Item 2\n\n- Item 3\n";
584 assert!(check(content, ListItemSpacingStyle::Consistent).is_empty());
585 }
586
587 #[test]
588 fn consistent_mixed_majority_loose_warns_tight() {
589 let content = "- Item 1\n\n- Item 2\n- Item 3\n\n- Item 4\n";
591 let warnings = check(content, ListItemSpacingStyle::Consistent);
592 assert_eq!(warnings.len(), 1);
593 assert!(warnings[0].message.contains("Missing"));
594 }
595
596 #[test]
597 fn consistent_mixed_majority_tight_warns_loose() {
598 let content = "- Item 1\n\n- Item 2\n- Item 3\n- Item 4\n";
600 let warnings = check(content, ListItemSpacingStyle::Consistent);
601 assert_eq!(warnings.len(), 1);
602 assert!(warnings[0].message.contains("Unexpected"));
603 }
604
605 #[test]
606 fn consistent_tie_prefers_tight() {
607 let content = "- Item 1\n\n- Item 2\n- Item 3\n";
611 let warnings = check(content, ListItemSpacingStyle::Consistent);
612 assert_eq!(warnings.len(), 1);
613 assert!(warnings[0].message.contains("Unexpected"));
614 }
615
616 #[test]
619 fn single_item_list_no_warnings() {
620 let content = "- Only item\n";
621 assert!(check(content, ListItemSpacingStyle::Loose).is_empty());
622 assert!(check(content, ListItemSpacingStyle::Tight).is_empty());
623 assert!(check(content, ListItemSpacingStyle::Consistent).is_empty());
624 }
625
626 #[test]
627 fn empty_content_no_warnings() {
628 assert!(check("", ListItemSpacingStyle::Consistent).is_empty());
629 }
630
631 #[test]
632 fn ordered_list_tight_gaps_loose_style_warns() {
633 let content = "1. First\n2. Second\n3. Third\n";
634 let warnings = check(content, ListItemSpacingStyle::Loose);
635 assert_eq!(warnings.len(), 2);
636 }
637
638 #[test]
639 fn task_list_works() {
640 let content = "- [x] Task 1\n- [ ] Task 2\n- [x] Task 3\n";
641 let warnings = check(content, ListItemSpacingStyle::Loose);
642 assert_eq!(warnings.len(), 2);
643 let fixed = fix(content, ListItemSpacingStyle::Loose);
644 assert_eq!(fixed, "- [x] Task 1\n\n- [ ] Task 2\n\n- [x] Task 3\n");
645 }
646
647 #[test]
648 fn no_trailing_newline() {
649 let content = "- Item 1\n- Item 2";
650 let warnings = check(content, ListItemSpacingStyle::Loose);
651 assert_eq!(warnings.len(), 1);
652 let fixed = fix(content, ListItemSpacingStyle::Loose);
653 assert_eq!(fixed, "- Item 1\n\n- Item 2");
654 }
655
656 #[test]
657 fn two_separate_lists() {
658 let content = "- A\n- B\n\nText\n\n1. One\n2. Two\n";
659 let warnings = check(content, ListItemSpacingStyle::Loose);
660 assert_eq!(warnings.len(), 2);
661 let fixed = fix(content, ListItemSpacingStyle::Loose);
662 assert_eq!(fixed, "- A\n\n- B\n\nText\n\n1. One\n\n2. Two\n");
663 }
664
665 #[test]
666 fn no_list_content() {
667 let content = "Just a paragraph.\n\nAnother paragraph.\n";
668 assert!(check(content, ListItemSpacingStyle::Loose).is_empty());
669 assert!(check(content, ListItemSpacingStyle::Tight).is_empty());
670 }
671
672 #[test]
675 fn continuation_lines_tight_detected() {
676 let content = "- Item 1\n continuation\n- Item 2\n";
677 let warnings = check(content, ListItemSpacingStyle::Loose);
678 assert_eq!(warnings.len(), 1);
679 assert!(warnings[0].message.contains("Missing"));
680 }
681
682 #[test]
683 fn continuation_lines_loose_detected() {
684 let content = "- Item 1\n continuation\n\n- Item 2\n";
685 assert!(check(content, ListItemSpacingStyle::Loose).is_empty());
686 let warnings = check(content, ListItemSpacingStyle::Tight);
687 assert_eq!(warnings.len(), 1);
688 assert!(warnings[0].message.contains("Unexpected"));
689 }
690
691 #[test]
692 fn multi_paragraph_item_not_treated_as_inter_item_gap() {
693 let content = "- Item 1\n\n Second paragraph\n\n- Item 2\n";
696 let warnings = check(content, ListItemSpacingStyle::Tight);
698 assert_eq!(
699 warnings.len(),
700 1,
701 "Should warn only on the inter-item blank, not the intra-item blank"
702 );
703 let fixed = fix(content, ListItemSpacingStyle::Tight);
706 assert_eq!(fixed, "- Item 1\n\n Second paragraph\n- Item 2\n");
707 }
708
709 #[test]
710 fn multi_paragraph_item_loose_style_no_warnings() {
711 let content = "- Item 1\n\n Second paragraph\n\n- Item 2\n";
713 assert!(check(content, ListItemSpacingStyle::Loose).is_empty());
714 }
715
716 #[test]
719 fn blockquote_tight_list_loose_style_warns() {
720 let content = "> - Item 1\n> - Item 2\n> - Item 3\n";
721 let warnings = check(content, ListItemSpacingStyle::Loose);
722 assert_eq!(warnings.len(), 2);
723 }
724
725 #[test]
726 fn blockquote_loose_list_detected() {
727 let content = "> - Item 1\n>\n> - Item 2\n";
729 let warnings = check(content, ListItemSpacingStyle::Tight);
730 assert_eq!(warnings.len(), 1, "Blockquote-only line should be detected as blank");
731 assert!(warnings[0].message.contains("Unexpected"));
732 }
733
734 #[test]
735 fn blockquote_loose_list_no_warnings_when_loose() {
736 let content = "> - Item 1\n>\n> - Item 2\n";
737 assert!(check(content, ListItemSpacingStyle::Loose).is_empty());
738 }
739
740 #[test]
743 fn multiple_blanks_all_removed() {
744 let content = "- Item 1\n\n\n- Item 2\n";
745 let fixed = fix(content, ListItemSpacingStyle::Tight);
746 assert_eq!(fixed, "- Item 1\n- Item 2\n");
747 }
748
749 #[test]
750 fn multiple_blanks_fix_is_idempotent() {
751 let content = "- Item 1\n\n\n\n- Item 2\n";
752 let fixed_once = fix(content, ListItemSpacingStyle::Tight);
753 let fixed_twice = fix(&fixed_once, ListItemSpacingStyle::Tight);
754 assert_eq!(fixed_once, fixed_twice);
755 assert_eq!(fixed_once, "- Item 1\n- Item 2\n");
756 }
757
758 #[test]
761 fn fix_adds_blank_lines() {
762 let content = "- Item 1\n- Item 2\n- Item 3\n";
763 let fixed = fix(content, ListItemSpacingStyle::Loose);
764 assert_eq!(fixed, "- Item 1\n\n- Item 2\n\n- Item 3\n");
765 }
766
767 #[test]
768 fn fix_removes_blank_lines() {
769 let content = "- Item 1\n\n- Item 2\n\n- Item 3\n";
770 let fixed = fix(content, ListItemSpacingStyle::Tight);
771 assert_eq!(fixed, "- Item 1\n- Item 2\n- Item 3\n");
772 }
773
774 #[test]
775 fn fix_consistent_adds_blank() {
776 let content = "- Item 1\n\n- Item 2\n- Item 3\n\n- Item 4\n";
778 let fixed = fix(content, ListItemSpacingStyle::Consistent);
779 assert_eq!(fixed, "- Item 1\n\n- Item 2\n\n- Item 3\n\n- Item 4\n");
780 }
781
782 #[test]
783 fn fix_idempotent_loose() {
784 let content = "- Item 1\n- Item 2\n";
785 let fixed_once = fix(content, ListItemSpacingStyle::Loose);
786 let fixed_twice = fix(&fixed_once, ListItemSpacingStyle::Loose);
787 assert_eq!(fixed_once, fixed_twice);
788 }
789
790 #[test]
791 fn fix_idempotent_tight() {
792 let content = "- Item 1\n\n- Item 2\n";
793 let fixed_once = fix(content, ListItemSpacingStyle::Tight);
794 let fixed_twice = fix(&fixed_once, ListItemSpacingStyle::Tight);
795 assert_eq!(fixed_once, fixed_twice);
796 }
797
798 #[test]
801 fn nested_list_does_not_affect_parent() {
802 let content = "- Item 1\n - Nested A\n - Nested B\n- Item 2\n";
804 let warnings = check(content, ListItemSpacingStyle::Tight);
805 assert!(
806 warnings.is_empty(),
807 "Nested items should not cause parent-level warnings"
808 );
809 }
810
811 #[test]
814 fn code_block_in_tight_list_no_false_positive() {
815 let content = "\
817- Item 1 with code:
818
819 ```python
820 print('hello')
821 ```
822
823- Item 2 simple.
824- Item 3 simple.
825";
826 assert!(
827 check(content, ListItemSpacingStyle::Consistent).is_empty(),
828 "Structural blank after code block should not make item 1 appear loose"
829 );
830 }
831
832 #[test]
833 fn table_in_tight_list_no_false_positive() {
834 let content = "\
836- Item 1 with table:
837
838 | Col 1 | Col 2 |
839 |-------|-------|
840 | A | B |
841
842- Item 2 simple.
843- Item 3 simple.
844";
845 assert!(
846 check(content, ListItemSpacingStyle::Consistent).is_empty(),
847 "Structural blank after table should not make item 1 appear loose"
848 );
849 }
850
851 #[test]
852 fn html_block_in_tight_list_no_false_positive() {
853 let content = "\
854- Item 1 with HTML:
855
856 <details>
857 <summary>Click</summary>
858 Content
859 </details>
860
861- Item 2 simple.
862- Item 3 simple.
863";
864 assert!(
865 check(content, ListItemSpacingStyle::Consistent).is_empty(),
866 "Structural blank after HTML block should not make item 1 appear loose"
867 );
868 }
869
870 #[test]
871 fn blockquote_in_tight_list_no_false_positive() {
872 let content = "\
874- Item 1 with quote:
875
876 > This is a blockquote
877 > with multiple lines.
878
879- Item 2 simple.
880- Item 3 simple.
881";
882 assert!(
883 check(content, ListItemSpacingStyle::Consistent).is_empty(),
884 "Structural blank around blockquote should not make item 1 appear loose"
885 );
886 assert!(
887 check(content, ListItemSpacingStyle::Tight).is_empty(),
888 "Blockquote in tight list should not trigger a violation"
889 );
890 }
891
892 #[test]
893 fn blockquote_multiple_items_with_quotes_tight() {
894 let content = "\
896- Item 1:
897
898 > Quote A
899
900- Item 2:
901
902 > Quote B
903
904- Item 3 plain.
905";
906 assert!(
907 check(content, ListItemSpacingStyle::Tight).is_empty(),
908 "Multiple items with blockquotes should remain tight"
909 );
910 }
911
912 #[test]
913 fn blockquote_mixed_with_genuine_loose_gap() {
914 let content = "\
916- Item 1:
917
918 > Quote
919
920- Item 2 plain.
921
922- Item 3 plain.
923";
924 let warnings = check(content, ListItemSpacingStyle::Tight);
925 assert!(
926 !warnings.is_empty(),
927 "Genuine loose gap between Item 2 and Item 3 should be flagged"
928 );
929 }
930
931 #[test]
932 fn blockquote_single_line_in_tight_list() {
933 let content = "\
934- Item 1:
935
936 > Single line quote.
937
938- Item 2.
939- Item 3.
940";
941 assert!(
942 check(content, ListItemSpacingStyle::Tight).is_empty(),
943 "Single-line blockquote should be structural"
944 );
945 }
946
947 #[test]
948 fn blockquote_in_ordered_list_tight() {
949 let content = "\
9501. Item 1:
951
952 > Quoted text in ordered list.
953
9541. Item 2.
9551. Item 3.
956";
957 assert!(
958 check(content, ListItemSpacingStyle::Tight).is_empty(),
959 "Blockquote in ordered list should be structural"
960 );
961 }
962
963 #[test]
964 fn nested_blockquote_in_tight_list() {
965 let content = "\
966- Item 1:
967
968 > Outer quote
969 > > Nested quote
970
971- Item 2.
972- Item 3.
973";
974 assert!(
975 check(content, ListItemSpacingStyle::Tight).is_empty(),
976 "Nested blockquote in tight list should be structural"
977 );
978 }
979
980 #[test]
981 fn blockquote_as_entire_item_is_loose() {
982 let content = "\
985- > Quote is the entire item content.
986
987- Item 2.
988- Item 3.
989";
990 let warnings = check(content, ListItemSpacingStyle::Tight);
991 assert!(
992 !warnings.is_empty(),
993 "Blank after blockquote-only item is a genuine loose gap"
994 );
995 }
996
997 #[test]
998 fn mixed_code_and_table_in_tight_list() {
999 let content = "\
10001. Item with code:
1001
1002 ```markdown
1003 This is some Markdown
1004 ```
1005
10061. Simple item.
10071. Item with table:
1008
1009 | Col 1 | Col 2 |
1010 |:------|:------|
1011 | Row 1 | Row 1 |
1012 | Row 2 | Row 2 |
1013";
1014 assert!(
1015 check(content, ListItemSpacingStyle::Consistent).is_empty(),
1016 "Mix of code blocks and tables should not cause false positives"
1017 );
1018 }
1019
1020 #[test]
1021 fn code_block_with_genuinely_loose_gaps_still_warns() {
1022 let content = "\
1025- Item 1:
1026
1027 ```bash
1028 echo hi
1029 ```
1030
1031- Item 2
1032
1033- Item 3
1034- Item 4
1035";
1036 let warnings = check(content, ListItemSpacingStyle::Consistent);
1037 assert!(
1038 !warnings.is_empty(),
1039 "Genuine inconsistency with code blocks should still be flagged"
1040 );
1041 }
1042
1043 #[test]
1044 fn all_items_have_code_blocks_no_warnings() {
1045 let content = "\
1046- Item 1:
1047
1048 ```python
1049 print(1)
1050 ```
1051
1052- Item 2:
1053
1054 ```python
1055 print(2)
1056 ```
1057
1058- Item 3:
1059
1060 ```python
1061 print(3)
1062 ```
1063";
1064 assert!(
1065 check(content, ListItemSpacingStyle::Consistent).is_empty(),
1066 "All items with code blocks should be consistently tight"
1067 );
1068 }
1069
1070 #[test]
1071 fn tilde_fence_code_block_in_list() {
1072 let content = "\
1073- Item 1:
1074
1075 ~~~
1076 code here
1077 ~~~
1078
1079- Item 2 simple.
1080- Item 3 simple.
1081";
1082 assert!(
1083 check(content, ListItemSpacingStyle::Consistent).is_empty(),
1084 "Tilde fences should be recognized as structural content"
1085 );
1086 }
1087
1088 #[test]
1089 fn nested_list_with_code_block() {
1090 let content = "\
1091- Item 1
1092 - Nested with code:
1093
1094 ```
1095 nested code
1096 ```
1097
1098 - Nested simple.
1099- Item 2
1100";
1101 assert!(
1102 check(content, ListItemSpacingStyle::Consistent).is_empty(),
1103 "Nested list with code block should not cause false positives"
1104 );
1105 }
1106
1107 #[test]
1108 fn tight_style_with_code_block_no_warnings() {
1109 let content = "\
1110- Item 1:
1111
1112 ```
1113 code
1114 ```
1115
1116- Item 2.
1117- Item 3.
1118";
1119 assert!(
1120 check(content, ListItemSpacingStyle::Tight).is_empty(),
1121 "Tight style should not warn about structural blanks around code blocks"
1122 );
1123 }
1124
1125 #[test]
1126 fn loose_style_with_code_block_missing_separator() {
1127 let content = "\
1130- Item 1:
1131
1132 ```
1133 code
1134 ```
1135
1136- Item 2.
1137- Item 3.
1138";
1139 let warnings = check(content, ListItemSpacingStyle::Loose);
1140 assert_eq!(
1141 warnings.len(),
1142 1,
1143 "Loose style should still require blank between simple items"
1144 );
1145 assert!(warnings[0].message.contains("Missing"));
1146 }
1147
1148 #[test]
1149 fn blockquote_list_with_code_block() {
1150 let content = "\
1151> - Item 1:
1152>
1153> ```
1154> code
1155> ```
1156>
1157> - Item 2.
1158> - Item 3.
1159";
1160 assert!(
1161 check(content, ListItemSpacingStyle::Consistent).is_empty(),
1162 "Blockquote-prefixed list with code block should not cause false positives"
1163 );
1164 }
1165
1166 #[test]
1169 fn indented_code_block_in_list_no_false_positive() {
1170 let content = "\
11731. Item with indented code:
1174
1175 some code here
1176 more code
1177
11781. Simple item
11791. Another item
1180";
1181 assert!(
1182 check(content, ListItemSpacingStyle::Consistent).is_empty(),
1183 "Structural blank after indented code block should not make item 1 appear loose"
1184 );
1185 }
1186
1187 #[test]
1190 fn fence_on_marker_line_keeps_its_structural_blank() {
1191 for spaces in 1..=4 {
1196 let pad = " ".repeat(spaces);
1197 let indent = " ".repeat(spaces + 1);
1198 let content = format!("- a\n\n-{pad}```\n{indent}code\n{indent}```\n- c\n");
1199 assert!(
1200 check(&content, ListItemSpacingStyle::Tight).is_empty(),
1201 "a fence on the marker line with {spaces} space(s) opens a fenced block, so its blank is structural"
1202 );
1203 assert_eq!(
1204 fix(&content, ListItemSpacingStyle::Tight),
1205 content,
1206 "tight fix must keep the blank MD031 requires ({spaces} space(s))"
1207 );
1208 }
1209 }
1210
1211 #[test]
1212 fn over_indented_fence_on_marker_line_is_an_indented_block_not_an_exemption() {
1213 for fence in ["```", "~~~"] {
1218 let content = format!("- a\n\n- {fence}\n code\n {fence}\n- c\n");
1219 let warnings = check(&content, ListItemSpacingStyle::Tight);
1220 assert_eq!(
1221 warnings.len(),
1222 1,
1223 "no fenced block starts here, so the blank is a loose gap ({fence}): {warnings:?}"
1224 );
1225 assert_eq!(
1226 fix(&content, ListItemSpacingStyle::Tight),
1227 format!("- a\n- {fence}\n code\n {fence}\n- c\n"),
1228 "tight fix must remove a blank that MD031 does not require ({fence})"
1229 );
1230 }
1231 }
1232
1233 #[test]
1236 fn code_block_in_middle_of_item_text_after_is_genuinely_loose() {
1237 let content = "\
12421. Item with code in middle:
1243
1244 ```
1245 code
1246 ```
1247
1248 Some text after the code block.
1249
12501. Simple item
12511. Another item
1252";
1253 let warnings = check(content, ListItemSpacingStyle::Consistent);
1254 assert!(
1255 !warnings.is_empty(),
1256 "Blank line after regular text (not structural content) is a genuine loose gap"
1257 );
1258 }
1259
1260 #[test]
1263 fn tight_fix_preserves_structural_blanks_around_code_blocks() {
1264 let content = "\
1267- Item 1:
1268
1269 ```
1270 code
1271 ```
1272
1273- Item 2.
1274- Item 3.
1275";
1276 let fixed = fix(content, ListItemSpacingStyle::Tight);
1277 assert_eq!(
1278 fixed, content,
1279 "Tight fix should not remove structural blanks around code blocks"
1280 );
1281 }
1282
1283 #[test]
1286 fn four_space_indented_fence_in_loose_list_no_false_positive() {
1287 let content = "\
12921. First item
1293
12941. Second item with code block:
1295
1296 ```json
1297 {\"key\": \"value\"}
1298 ```
1299
13001. Third item
1301";
1302 assert!(
1303 check(content, ListItemSpacingStyle::Consistent).is_empty(),
1304 "Structural blank after 4-space indented code block should not cause false positive"
1305 );
1306 }
1307
1308 #[test]
1309 fn four_space_indented_fence_tight_style_no_warnings() {
1310 let content = "\
13111. First item
13121. Second item with code block:
1313
1314 ```json
1315 {\"key\": \"value\"}
1316 ```
1317
13181. Third item
1319";
1320 assert!(
1321 check(content, ListItemSpacingStyle::Tight).is_empty(),
1322 "Tight style should not warn about structural blanks with 4-space fences"
1323 );
1324 }
1325
1326 #[test]
1327 fn four_space_indented_fence_loose_style_no_warnings() {
1328 let content = "\
13301. First item
1331
13321. Second item with code block:
1333
1334 ```json
1335 {\"key\": \"value\"}
1336 ```
1337
13381. Third item
1339";
1340 assert!(
1341 check(content, ListItemSpacingStyle::Loose).is_empty(),
1342 "Loose style should not warn when structural gaps are the only non-loose gaps"
1343 );
1344 }
1345
1346 #[test]
1347 fn structural_gap_with_genuine_inconsistency_still_warns() {
1348 let content = "\
13511. First item with code:
1352
1353 ```json
1354 {\"key\": \"value\"}
1355 ```
1356
13571. Second item
1358
13591. Third item
13601. Fourth item
1361";
1362 let warnings = check(content, ListItemSpacingStyle::Consistent);
1363 assert!(
1364 !warnings.is_empty(),
1365 "Genuine loose/tight inconsistency should still warn even with structural gaps"
1366 );
1367 }
1368
1369 #[test]
1370 fn four_space_fence_fix_is_idempotent() {
1371 let content = "\
13741. First item
1375
13761. Second item with code block:
1377
1378 ```json
1379 {\"key\": \"value\"}
1380 ```
1381
13821. Third item
1383";
1384 let fixed = fix(content, ListItemSpacingStyle::Consistent);
1385 assert_eq!(fixed, content, "Fix should be a no-op for lists with structural gaps");
1386 let fixed_twice = fix(&fixed, ListItemSpacingStyle::Consistent);
1387 assert_eq!(fixed, fixed_twice, "Fix should be idempotent");
1388 }
1389
1390 #[test]
1391 fn four_space_fence_fix_does_not_insert_duplicate_blank() {
1392 let content = "\
13951. First item
13961. Second item with code block:
1397
1398 ```json
1399 {\"key\": \"value\"}
1400 ```
1401
14021. Third item
1403";
1404 let fixed = fix(content, ListItemSpacingStyle::Tight);
1405 assert_eq!(fixed, content, "Tight fix should not modify structural blanks");
1406 }
1407
1408 #[test]
1409 fn mkdocs_flavor_code_block_in_list_no_false_positive() {
1410 let content = "\
14131. First item
1414
14151. Second item with code block:
1416
1417 ```json
1418 {\"key\": \"value\"}
1419 ```
1420
14211. Third item
1422";
1423 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1424 let rule = MD076ListItemSpacing::new(ListItemSpacingStyle::Consistent);
1425 let warnings = rule.check(&ctx).unwrap();
1426 assert!(
1427 warnings.is_empty(),
1428 "MkDocs flavor with structural code block blank should not produce false positive, got: {warnings:?}"
1429 );
1430 }
1431
1432 #[test]
1435 fn code_block_in_second_item_detects_inconsistency() {
1436 let content = "\
1439# Test
1440
1441- Lorem ipsum dolor sit amet.
1442- Lorem ipsum dolor sit amet.
1443
1444 ```yaml
1445 hello: world
1446 ```
1447
1448- Lorem ipsum dolor sit amet.
1449
1450- Lorem ipsum dolor sit amet.
1451";
1452 let warnings = check(content, ListItemSpacingStyle::Consistent);
1453 assert!(
1454 !warnings.is_empty(),
1455 "Should detect inconsistent spacing when code block is inside a list item"
1456 );
1457 }
1458
1459 #[test]
1460 fn code_block_in_item_all_tight_no_warnings() {
1461 let content = "\
1463- Item 1
1464- Item 2
1465
1466 ```yaml
1467 hello: world
1468 ```
1469
1470- Item 3
1471- Item 4
1472";
1473 assert!(
1474 check(content, ListItemSpacingStyle::Consistent).is_empty(),
1475 "All tight gaps with structural code block should not warn"
1476 );
1477 }
1478
1479 #[test]
1480 fn code_block_in_item_all_loose_no_warnings() {
1481 let content = "\
1483- Item 1
1484
1485- Item 2
1486
1487 ```yaml
1488 hello: world
1489 ```
1490
1491- Item 3
1492
1493- Item 4
1494";
1495 assert!(
1496 check(content, ListItemSpacingStyle::Consistent).is_empty(),
1497 "All loose gaps with structural code block should not warn"
1498 );
1499 }
1500
1501 #[test]
1502 fn code_block_in_ordered_list_detects_inconsistency() {
1503 let content = "\
15041. First item
15051. Second item
1506
1507 ```json
1508 {\"key\": \"value\"}
1509 ```
1510
15111. Third item
1512
15131. Fourth item
1514";
1515 let warnings = check(content, ListItemSpacingStyle::Consistent);
1516 assert!(
1517 !warnings.is_empty(),
1518 "Ordered list with code block should still detect inconsistency"
1519 );
1520 }
1521
1522 #[test]
1523 fn code_block_in_item_fix_removes_loose_outlier_on_tie() {
1524 let content = "\
1530- Item 1
1531- Item 2
1532
1533 ```yaml
1534 code: here
1535 ```
1536
1537- Item 3
1538
1539- Item 4
1540";
1541 let fixed = fix(content, ListItemSpacingStyle::Consistent);
1542 assert!(
1543 fixed.contains("- Item 3\n- Item 4"),
1544 "Fix should remove blank line between items 3 and 4. Got:\n{fixed}"
1545 );
1546 assert!(
1547 !fixed.contains("- Item 1\n\n- Item 2"),
1548 "Fix should not insert a blank between items 1 and 2. Got:\n{fixed}"
1549 );
1550 }
1551
1552 #[test]
1553 fn tilde_code_block_in_item_detects_inconsistency() {
1554 let content = "\
1555- Item 1
1556- Item 2
1557
1558 ~~~
1559 code
1560 ~~~
1561
1562- Item 3
1563
1564- Item 4
1565";
1566 let warnings = check(content, ListItemSpacingStyle::Consistent);
1567 assert!(
1568 !warnings.is_empty(),
1569 "Tilde code block inside item should not prevent inconsistency detection"
1570 );
1571 }
1572
1573 #[test]
1574 fn multiple_code_blocks_all_tight_no_warnings() {
1575 let content = "\
1577- Item 1
1578
1579 ```
1580 code1
1581 ```
1582
1583- Item 2
1584
1585 ```
1586 code2
1587 ```
1588
1589- Item 3
1590- Item 4
1591";
1592 assert!(
1593 check(content, ListItemSpacingStyle::Consistent).is_empty(),
1594 "All non-structural gaps are tight, so list is consistent"
1595 );
1596 }
1597
1598 #[test]
1599 fn code_block_with_mixed_genuine_gaps_warns() {
1600 let content = "\
1602- Item 1
1603
1604 ```
1605 code1
1606 ```
1607
1608- Item 2
1609
1610- Item 3
1611- Item 4
1612";
1613 let warnings = check(content, ListItemSpacingStyle::Consistent);
1614 assert!(
1615 !warnings.is_empty(),
1616 "Mixed genuine gaps (loose + tight) with structural code block should still warn"
1617 );
1618 }
1619
1620 #[test]
1623 fn continuation_loose_tight_style_default_warns() {
1624 let content = "\
1627- Item 1.
1628
1629 Continuation paragraph.
1630
1631- Item 2.
1632
1633 Continuation paragraph.
1634
1635- Item 3.
1636";
1637 let warnings = check_with_continuation(content, ListItemSpacingStyle::Tight, false);
1638 assert!(
1639 !warnings.is_empty(),
1640 "Should warn about loose gaps when allow_loose_continuation is false"
1641 );
1642 }
1643
1644 #[test]
1645 fn continuation_loose_tight_style_allowed_no_warnings() {
1646 let content = "\
1649- Item 1.
1650
1651 Continuation paragraph.
1652
1653- Item 2.
1654
1655 Continuation paragraph.
1656
1657- Item 3.
1658";
1659 let warnings = check_with_continuation(content, ListItemSpacingStyle::Tight, true);
1660 assert!(
1661 warnings.is_empty(),
1662 "Should not warn when allow_loose_continuation is true, got: {warnings:?}"
1663 );
1664 }
1665
1666 #[test]
1667 fn continuation_loose_mixed_items_warns() {
1668 let content = "\
1671- Item 1.
1672
1673- Item 2.
1674- Item 3.
1675";
1676 let warnings = check_with_continuation(content, ListItemSpacingStyle::Tight, true);
1677 assert!(
1678 !warnings.is_empty(),
1679 "Genuine loose gaps should still warn even with allow_loose_continuation"
1680 );
1681 }
1682
1683 #[test]
1684 fn continuation_loose_consistent_mode() {
1685 let content = "\
1688- Item 1.
1689
1690 Continuation paragraph.
1691
1692- Item 2.
1693- Item 3.
1694";
1695 let warnings = check_with_continuation(content, ListItemSpacingStyle::Consistent, true);
1696 assert!(
1697 warnings.is_empty(),
1698 "Continuation gaps should not affect consistency when allowed, got: {warnings:?}"
1699 );
1700 }
1701
1702 #[test]
1703 fn continuation_loose_fix_preserves_continuation_blanks() {
1704 let content = "\
1705- Item 1.
1706
1707 Continuation paragraph.
1708
1709- Item 2.
1710
1711 Continuation paragraph.
1712
1713- Item 3.
1714";
1715 let fixed = fix_with_continuation(content, ListItemSpacingStyle::Tight, true);
1716 assert_eq!(fixed, content, "Fix should preserve continuation blank lines");
1717 }
1718
1719 #[test]
1720 fn continuation_loose_fix_removes_genuine_loose_gaps() {
1721 let input = "\
1722- Item 1.
1723
1724- Item 2.
1725
1726- Item 3.
1727";
1728 let expected = "\
1729- Item 1.
1730- Item 2.
1731- Item 3.
1732";
1733 let fixed = fix_with_continuation(input, ListItemSpacingStyle::Tight, true);
1734 assert_eq!(fixed, expected);
1735 }
1736
1737 #[test]
1738 fn continuation_loose_ordered_list() {
1739 let content = "\
17401. Item 1.
1741
1742 Continuation paragraph.
1743
17442. Item 2.
1745
1746 Continuation paragraph.
1747
17483. Item 3.
1749";
1750 let warnings = check_with_continuation(content, ListItemSpacingStyle::Tight, true);
1751 assert!(
1752 warnings.is_empty(),
1753 "Ordered list continuation should work too, got: {warnings:?}"
1754 );
1755 }
1756
1757 #[test]
1758 fn continuation_loose_disabled_by_default() {
1759 let rule = MD076ListItemSpacing::new(ListItemSpacingStyle::Tight);
1761 assert!(!rule.config.allow_loose_continuation);
1762 }
1763
1764 #[test]
1765 fn continuation_loose_ordered_under_indented_warns() {
1766 let content = "\
17691. Item 1.
1770
1771 Under-indented text.
1772
17731. Item 2.
17741. Item 3.
1775";
1776 let warnings = check_with_continuation(content, ListItemSpacingStyle::Tight, true);
1777 assert!(
1778 !warnings.is_empty(),
1779 "Under-indented text should not be treated as continuation, got: {warnings:?}"
1780 );
1781 }
1782
1783 #[test]
1784 fn continuation_loose_mix_continuation_and_genuine_gaps() {
1785 let content = "\
1787- Item 1.
1788
1789 Continuation paragraph.
1790
1791- Item 2.
1792
1793- Item 3.
1794";
1795 let warnings = check_with_continuation(content, ListItemSpacingStyle::Tight, true);
1796 assert!(
1797 !warnings.is_empty(),
1798 "Genuine loose gap between items 2-3 should warn even with continuation allowed"
1799 );
1800 assert_eq!(
1802 warnings.len(),
1803 1,
1804 "Expected exactly one warning for the genuine loose gap"
1805 );
1806 }
1807
1808 #[test]
1809 fn continuation_loose_fix_mixed_preserves_continuation_removes_genuine() {
1810 let input = "\
1812- Item 1.
1813
1814 Continuation paragraph.
1815
1816- Item 2.
1817
1818- Item 3.
1819";
1820 let expected = "\
1821- Item 1.
1822
1823 Continuation paragraph.
1824
1825- Item 2.
1826- Item 3.
1827";
1828 let fixed = fix_with_continuation(input, ListItemSpacingStyle::Tight, true);
1829 assert_eq!(fixed, expected);
1830 }
1831
1832 #[test]
1833 fn continuation_loose_after_code_block() {
1834 let content = "\
1836- Item 1.
1837
1838 ```python
1839 code
1840 ```
1841
1842 Continuation after code.
1843
1844- Item 2.
1845- Item 3.
1846";
1847 let warnings = check_with_continuation(content, ListItemSpacingStyle::Tight, true);
1848 assert!(
1849 warnings.is_empty(),
1850 "Code block + continuation should both be exempt, got: {warnings:?}"
1851 );
1852 }
1853
1854 #[test]
1855 fn continuation_loose_style_does_not_interfere() {
1856 let content = "\
1859- Item 1.
1860
1861 Continuation paragraph.
1862
1863- Item 2.
1864
1865 Continuation paragraph.
1866
1867- Item 3.
1868";
1869 let warnings = check_with_continuation(content, ListItemSpacingStyle::Loose, true);
1870 assert!(
1871 warnings.is_empty(),
1872 "Loose style with continuation should not warn, got: {warnings:?}"
1873 );
1874 }
1875
1876 #[test]
1877 fn continuation_loose_tight_no_continuation_content() {
1878 let content = "\
1880- Item 1.
1881- Item 2.
1882- Item 3.
1883";
1884 let warnings = check_with_continuation(content, ListItemSpacingStyle::Tight, true);
1885 assert!(
1886 warnings.is_empty(),
1887 "Simple tight list should pass with allow_loose_continuation, got: {warnings:?}"
1888 );
1889 }
1890
1891 #[test]
1894 fn default_config_section_provides_style_key() {
1895 let rule = MD076ListItemSpacing::new(ListItemSpacingStyle::Consistent);
1896 let section = rule.default_config_section();
1897 assert!(section.is_some());
1898 let (name, value) = section.unwrap();
1899 assert_eq!(name, "MD076");
1900 if let toml::Value::Table(map) = value {
1901 assert!(map.contains_key("style"));
1902 assert!(map.contains_key("allow-loose-continuation"));
1903 } else {
1904 panic!("Expected Table value from default_config_section");
1905 }
1906 }
1907}