1use crate::lint_context::LazyContLine;
2use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
3use crate::utils::blockquote::{content_after_blockquote, effective_indent_in_blockquote};
4use crate::utils::calculate_indentation_width_default;
5use crate::utils::pandoc;
6use crate::utils::range_utils::{LineIndex, calculate_line_range};
7use crate::utils::regex_cache::BLOCKQUOTE_PREFIX_RE;
8use regex::Regex;
9use std::sync::LazyLock;
10
11mod md032_config;
12pub(super) use md032_config::MD032Config;
13
14static ORDERED_LIST_NON_ONE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*([2-9]|\d{2,})\.\s").unwrap());
16
17fn is_thematic_break(line: &str) -> bool {
20 if calculate_indentation_width_default(line) > 3 {
22 return false;
23 }
24
25 let trimmed = line.trim();
26 if trimmed.len() < 3 {
27 return false;
28 }
29
30 let chars: Vec<char> = trimmed.chars().collect();
31 let first_non_space = chars.iter().find(|&&c| c != ' ');
32
33 if let Some(&marker) = first_non_space {
34 if marker != '-' && marker != '*' && marker != '_' {
35 return false;
36 }
37 let marker_count = chars.iter().filter(|&&c| c == marker).count();
38 let other_count = chars.iter().filter(|&&c| c != marker && c != ' ').count();
39 marker_count >= 3 && other_count == 0
40 } else {
41 false
42 }
43}
44
45#[derive(Debug, Clone, Default)]
115pub struct MD032BlanksAroundLists {
116 config: MD032Config,
117}
118
119impl MD032BlanksAroundLists {
120 pub fn from_config_struct(config: MD032Config) -> Self {
121 Self { config }
122 }
123}
124
125impl MD032BlanksAroundLists {
126 fn should_require_blank_line_before(
128 ctx: &crate::lint_context::LintContext,
129 prev_line_num: usize,
130 current_line_num: usize,
131 ) -> bool {
132 if ctx
134 .line_info(prev_line_num)
135 .is_some_and(|info| info.in_code_block || info.in_front_matter)
136 {
137 return true;
138 }
139
140 if Self::is_nested_list(ctx, prev_line_num, current_line_num) {
142 return false;
143 }
144
145 true
147 }
148
149 fn is_nested_list(
151 ctx: &crate::lint_context::LintContext,
152 prev_line_num: usize, current_line_num: usize, ) -> bool {
155 if current_line_num > 0 && current_line_num - 1 < ctx.lines.len() {
157 let current_line = &ctx.lines[current_line_num - 1];
158 if current_line.indent >= 2 {
159 if prev_line_num > 0 && prev_line_num - 1 < ctx.lines.len() {
161 let prev_line = &ctx.lines[prev_line_num - 1];
162 if prev_line.list_item.is_some() || prev_line.indent >= 2 {
164 return true;
165 }
166 }
167 }
168 }
169 false
170 }
171
172 fn should_apply_lazy_fix(ctx: &crate::lint_context::LintContext, line_num: usize) -> bool {
175 ctx.lines
176 .get(line_num.saturating_sub(1))
177 .is_some_and(|li| !li.in_code_block && !li.in_front_matter && !li.in_html_comment && !li.in_mdx_comment)
178 }
179
180 fn calculate_lazy_continuation_fix(
183 ctx: &crate::lint_context::LintContext,
184 line_num: usize,
185 lazy_info: &LazyContLine,
186 ) -> Option<Fix> {
187 let line_info = ctx.lines.get(line_num.saturating_sub(1))?;
188 let line_content = line_info.content(ctx.content);
189
190 if lazy_info.blockquote_level == 0 {
191 let start_byte = line_info.byte_offset;
193 let end_byte = start_byte + lazy_info.current_indent;
194 let replacement = " ".repeat(lazy_info.expected_indent);
195
196 Some(Fix::new(start_byte..end_byte, replacement))
197 } else {
198 let after_bq = content_after_blockquote(line_content, lazy_info.blockquote_level);
200 let prefix_byte_len = line_content.len().saturating_sub(after_bq.len());
201 if prefix_byte_len == 0 {
202 return None;
203 }
204
205 let current_indent = after_bq.len() - after_bq.trim_start().len();
206 let start_byte = line_info.byte_offset + prefix_byte_len;
207 let end_byte = start_byte + current_indent;
208 let replacement = " ".repeat(lazy_info.expected_indent);
209
210 Some(Fix::new(start_byte..end_byte, replacement))
211 }
212 }
213
214 fn apply_lazy_fix_to_line(line: &str, lazy_info: &LazyContLine) -> String {
217 if lazy_info.blockquote_level == 0 {
218 let content = line.trim_start();
220 format!("{}{}", " ".repeat(lazy_info.expected_indent), content)
221 } else {
222 let after_bq = content_after_blockquote(line, lazy_info.blockquote_level);
224 let prefix_len = line.len().saturating_sub(after_bq.len());
225 if prefix_len == 0 {
226 return line.to_string();
227 }
228
229 let prefix = &line[..prefix_len];
230 let rest = after_bq.trim_start();
231 format!("{}{}{}", prefix, " ".repeat(lazy_info.expected_indent), rest)
232 }
233 }
234
235 fn find_preceding_content(ctx: &crate::lint_context::LintContext, before_line: usize) -> (usize, bool) {
243 let is_pandoc = ctx.flavor.is_pandoc_compatible();
244 for line_num in (1..before_line).rev() {
245 let idx = line_num - 1;
246 if let Some(info) = ctx.lines.get(idx) {
247 if info.in_html_comment || info.in_mdx_comment {
249 continue;
250 }
251 if is_pandoc {
253 let trimmed = info.content(ctx.content).trim();
254 if pandoc::is_div_open(trimmed) || pandoc::is_div_close(trimmed) {
255 continue;
256 }
257 }
258 return (line_num, info.is_blank);
259 }
260 }
261 (0, true)
263 }
264
265 fn find_following_content(ctx: &crate::lint_context::LintContext, after_line: usize) -> (usize, bool) {
272 let is_pandoc = ctx.flavor.is_pandoc_compatible();
273 let num_lines = ctx.lines.len();
274 for line_num in (after_line + 1)..=num_lines {
275 let idx = line_num - 1;
276 if let Some(info) = ctx.lines.get(idx) {
277 if info.in_html_comment || info.in_mdx_comment {
279 continue;
280 }
281 if is_pandoc {
283 let trimmed = info.content(ctx.content).trim();
284 if pandoc::is_div_open(trimmed) || pandoc::is_div_close(trimmed) {
285 continue;
286 }
287 }
288 return (line_num, info.is_blank);
289 }
290 }
291 (0, true)
293 }
294
295 fn convert_list_blocks(&self, ctx: &crate::lint_context::LintContext) -> Vec<(usize, usize, String)> {
297 let mut blocks: Vec<(usize, usize, String)> = Vec::new();
298
299 for block in &ctx.list_blocks {
300 if ctx
302 .line_info(block.start_line)
303 .is_some_and(|info| info.in_footnote_definition)
304 {
305 continue;
306 }
307
308 let mut segments: Vec<(usize, usize)> = Vec::new();
314 let mut current_start = block.start_line;
315 let mut prev_item_line = 0;
316
317 let get_blockquote_level = |line_num: usize| -> usize {
319 if line_num == 0 || line_num > ctx.lines.len() {
320 return 0;
321 }
322 let line_content = ctx.lines[line_num - 1].content(ctx.content);
323 BLOCKQUOTE_PREFIX_RE
324 .find(line_content)
325 .map_or(0, |m| m.as_str().chars().filter(|&c| c == '>').count())
326 };
327
328 let mut prev_bq_level = 0;
329
330 for &item_line in &block.item_lines {
331 let current_bq_level = get_blockquote_level(item_line);
332
333 if prev_item_line > 0 {
334 let blockquote_level_changed = prev_bq_level != current_bq_level;
336
337 let mut has_standalone_code_fence = false;
340
341 let min_indent_for_content = if block.is_ordered {
343 3 } else {
347 2 };
350
351 for check_line in (prev_item_line + 1)..item_line {
352 if check_line - 1 < ctx.lines.len() {
353 let line = &ctx.lines[check_line - 1];
354 let line_content = line.content(ctx.content);
355 if line.in_code_block
356 && (line_content.trim().starts_with("```") || line_content.trim().starts_with("~~~"))
357 {
358 if line.indent < min_indent_for_content {
361 has_standalone_code_fence = true;
362 break;
363 }
364 }
365 }
366 }
367
368 if has_standalone_code_fence || blockquote_level_changed {
369 segments.push((current_start, prev_item_line));
371 current_start = item_line;
372 }
373 }
374 prev_item_line = item_line;
375 prev_bq_level = current_bq_level;
376 }
377
378 if prev_item_line > 0 {
381 segments.push((current_start, prev_item_line));
382 }
383
384 let has_code_fence_splits = segments.len() > 1 && {
386 let mut found_fence = false;
388 for i in 0..segments.len() - 1 {
389 let seg_end = segments[i].1;
390 let next_start = segments[i + 1].0;
391 for check_line in (seg_end + 1)..next_start {
393 if check_line - 1 < ctx.lines.len() {
394 let line = &ctx.lines[check_line - 1];
395 let line_content = line.content(ctx.content);
396 if line.in_code_block
397 && (line_content.trim().starts_with("```") || line_content.trim().starts_with("~~~"))
398 {
399 found_fence = true;
400 break;
401 }
402 }
403 }
404 if found_fence {
405 break;
406 }
407 }
408 found_fence
409 };
410
411 for (start, end) in &segments {
413 let mut actual_end = *end;
415
416 if !has_code_fence_splits && *end < block.end_line {
419 let block_bq_level = block.blockquote_prefix.chars().filter(|&c| c == '>').count();
421
422 let min_continuation_indent = if block_bq_level > 0 {
425 if block.is_ordered {
427 block.max_marker_width
428 } else {
429 2 }
431 } else {
432 ctx.lines
433 .get(*end - 1)
434 .and_then(|line_info| line_info.list_item.as_ref())
435 .map_or(2, |item| item.content_column)
436 };
437
438 for check_line in (*end + 1)..=block.end_line {
439 if check_line - 1 < ctx.lines.len() {
440 let line = &ctx.lines[check_line - 1];
441 let line_content = line.content(ctx.content);
442 if block.item_lines.contains(&check_line) || line.heading.is_some() {
444 break;
445 }
446 if line.in_code_block {
448 break;
449 }
450
451 let effective_indent =
453 effective_indent_in_blockquote(line_content, block_bq_level, line.indent);
454
455 if effective_indent >= min_continuation_indent {
457 actual_end = check_line;
458 }
459 else if !line.is_blank
464 && line.heading.is_none()
465 && !block.item_lines.contains(&check_line)
466 && !is_thematic_break(line_content)
467 {
468 actual_end = check_line;
470 } else if !line.is_blank {
471 break;
473 }
474 }
475 }
476 }
477
478 blocks.push((*start, actual_end, block.blockquote_prefix.clone()));
479 }
480 }
481
482 blocks.retain(|(start, end, _)| {
484 let all_in_comment = (*start..=*end).all(|line_num| {
486 ctx.lines
487 .get(line_num - 1)
488 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
489 });
490 !all_in_comment
491 });
492
493 blocks
494 }
495
496 fn perform_checks(
497 &self,
498 ctx: &crate::lint_context::LintContext,
499 lines: &[&str],
500 list_blocks: &[(usize, usize, String)],
501 line_index: &LineIndex,
502 ) -> Vec<LintWarning> {
503 let mut warnings = Vec::new();
504 let num_lines = lines.len();
505
506 for (line_idx, line) in lines.iter().enumerate() {
509 let line_num = line_idx + 1;
510
511 let is_in_list = list_blocks
513 .iter()
514 .any(|(start, end, _)| line_num >= *start && line_num <= *end);
515 if is_in_list {
516 continue;
517 }
518
519 if ctx.line_info(line_num).is_some_and(|info| {
521 info.in_code_block
522 || info.in_front_matter
523 || info.in_html_comment
524 || info.in_mdx_comment
525 || info.in_html_block
526 || info.in_jsx_block
527 }) {
528 continue;
529 }
530
531 if ORDERED_LIST_NON_ONE_RE.is_match(line) {
533 if line_idx > 0 {
535 let prev_line = lines[line_idx - 1];
536 let prev_is_blank = is_blank_in_context(prev_line);
537 let prev_line_info = ctx.line_info(line_idx);
538 let prev_excluded = prev_line_info.is_some_and(|info| info.in_code_block || info.in_front_matter);
539
540 let prev_in_mkdocs_container =
556 prev_line_info.is_some_and(|info| info.in_admonition || info.in_content_tab);
557 let continues_stale_container_list = prev_in_mkdocs_container && {
558 let item_indent = calculate_indentation_width_default(line);
559 let mut found_marker = false;
560 for j in (0..line_idx).rev() {
561 let in_container = ctx
562 .line_info(j + 1)
563 .is_some_and(|info| info.in_admonition || info.in_content_tab);
564 if !in_container {
565 break;
566 }
567 let candidate = lines[j];
568 if is_blank_in_context(candidate) {
569 continue;
570 }
571 let candidate_indent = calculate_indentation_width_default(candidate);
572 if crate::utils::regex_cache::ORDERED_LIST_MARKER_REGEX.is_match(candidate)
573 && candidate_indent == item_indent
574 {
575 found_marker = true;
576 break;
577 }
578 if candidate_indent <= item_indent {
579 break;
580 }
581 }
582 found_marker
583 };
584
585 let prev_trimmed = prev_line.trim();
590 let is_sentence_continuation = continues_stale_container_list
591 || (!prev_is_blank
592 && !prev_trimmed.is_empty()
593 && !prev_trimmed.ends_with('.')
594 && !prev_trimmed.ends_with('!')
595 && !prev_trimmed.ends_with('?')
596 && !prev_trimmed.ends_with(':')
597 && !prev_trimmed.ends_with(';')
598 && !prev_trimmed.ends_with('>')
599 && !prev_trimmed.ends_with('-')
600 && !prev_trimmed.ends_with('*'));
601
602 if !prev_is_blank && !prev_excluded && !is_sentence_continuation {
603 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_num, line);
605
606 let bq_prefix = ctx.blockquote_prefix_for_blank_line(line_idx);
607 warnings.push(LintWarning {
608 line: start_line,
609 column: start_col,
610 end_line,
611 end_column: end_col,
612 severity: Severity::Warning,
613 rule_name: Some(self.name().to_string()),
614 message: "Ordered list starting with non-1 should be preceded by blank line".to_string(),
615 fix: Some(Fix::new(
616 line_index.line_col_to_byte_range_with_length(line_num, 1, 0),
617 format!("{bq_prefix}\n"),
618 )),
619 });
620 }
621
622 if line_idx + 1 < num_lines {
625 let next_line = lines[line_idx + 1];
626 let next_is_blank = is_blank_in_context(next_line);
627 let next_excluded = ctx.line_info(line_idx + 2).is_some_and(|info| info.in_front_matter);
628
629 if !next_is_blank && !next_excluded && !next_line.trim().is_empty() {
630 let next_trimmed = next_line.trim_start();
634 let next_is_ordered_content = ORDERED_LIST_NON_ONE_RE.is_match(next_line)
635 || next_line.starts_with("1. ")
636 || (next_line.len() > next_trimmed.len()
637 && !next_trimmed.starts_with("- ")
638 && !next_trimmed.starts_with("* ")
639 && !next_trimmed.starts_with("+ ")); if !next_is_ordered_content {
642 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_num, line);
643 let bq_prefix = ctx.blockquote_prefix_for_blank_line(line_idx);
644 warnings.push(LintWarning {
645 line: start_line,
646 column: start_col,
647 end_line,
648 end_column: end_col,
649 severity: Severity::Warning,
650 rule_name: Some(self.name().to_string()),
651 message: "List should be followed by blank line".to_string(),
652 fix: Some(Fix::new(
653 line_index.line_col_to_byte_range_with_length(line_num + 1, 1, 0),
654 format!("{bq_prefix}\n"),
655 )),
656 });
657 }
658 }
659 }
660 }
661 }
662 }
663
664 for &(start_line, end_line, ref prefix) in list_blocks {
665 if ctx
667 .line_info(start_line)
668 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
669 {
670 continue;
671 }
672
673 if start_line > 1 {
674 let (content_line, has_blank_separation) = Self::find_preceding_content(ctx, start_line);
676
677 if !has_blank_separation && content_line > 0 {
679 let prev_line_str = lines[content_line - 1];
680 let is_prev_excluded = ctx
681 .line_info(content_line)
682 .is_some_and(|info| info.in_code_block || info.in_front_matter);
683 let prev_prefix = BLOCKQUOTE_PREFIX_RE.find(prev_line_str).map_or("", |m| m.as_str());
684 let prefixes_match = prev_prefix.trim() == prefix.trim();
685
686 let should_require = Self::should_require_blank_line_before(ctx, content_line, start_line);
689 if !is_prev_excluded && prefixes_match && should_require {
690 let (start_line, start_col, end_line, end_col) =
692 calculate_line_range(start_line, lines[start_line - 1]);
693
694 warnings.push(LintWarning {
695 line: start_line,
696 column: start_col,
697 end_line,
698 end_column: end_col,
699 severity: Severity::Warning,
700 rule_name: Some(self.name().to_string()),
701 message: "List should be preceded by blank line".to_string(),
702 fix: Some(Fix::new(
703 line_index.line_col_to_byte_range_with_length(start_line, 1, 0),
704 format!("{prefix}\n"),
705 )),
706 });
707 }
708 }
709 }
710
711 if end_line < num_lines {
712 let (content_line, has_blank_separation) = Self::find_following_content(ctx, end_line);
714
715 if !has_blank_separation && content_line > 0 {
717 let next_line_str = lines[content_line - 1];
718 let is_next_excluded = ctx.line_info(content_line).is_some_and(|info| info.in_front_matter)
721 || (content_line <= ctx.lines.len()
722 && ctx.lines[content_line - 1].in_code_block
723 && ctx.lines[content_line - 1].indent >= 2);
724 let next_prefix = BLOCKQUOTE_PREFIX_RE.find(next_line_str).map_or("", |m| m.as_str());
725
726 let end_line_str = lines[end_line - 1];
731 let end_line_prefix = BLOCKQUOTE_PREFIX_RE.find(end_line_str).map_or("", |m| m.as_str());
732 let end_line_bq_level = end_line_prefix.chars().filter(|&c| c == '>').count();
733 let next_line_bq_level = next_prefix.chars().filter(|&c| c == '>').count();
734 let exits_blockquote = end_line_bq_level > 0 && next_line_bq_level < end_line_bq_level;
735
736 let prefixes_match = next_prefix.trim() == prefix.trim();
737
738 let is_tight_continuation_of_last_item = ctx
744 .lines
745 .get(end_line - 1)
746 .and_then(|last_li| last_li.list_item.as_ref())
747 .is_some_and(|last_item| {
748 let marker_col = last_item.marker_column;
749 ctx.lines.get(content_line - 1).is_some_and(|next_li| {
750 !next_li.is_blank && next_li.list_item.is_none() && next_li.indent > marker_col
751 })
752 });
753
754 if !is_next_excluded && prefixes_match && !exits_blockquote && !is_tight_continuation_of_last_item {
757 let (start_line_last, start_col_last, end_line_last, end_col_last) =
759 calculate_line_range(end_line, lines[end_line - 1]);
760
761 warnings.push(LintWarning {
762 line: start_line_last,
763 column: start_col_last,
764 end_line: end_line_last,
765 end_column: end_col_last,
766 severity: Severity::Warning,
767 rule_name: Some(self.name().to_string()),
768 message: "List should be followed by blank line".to_string(),
769 fix: Some(Fix::new(
770 line_index.line_col_to_byte_range_with_length(end_line + 1, 1, 0),
771 format!("{prefix}\n"),
772 )),
773 });
774 }
775 }
776 }
777 }
778 warnings
779 }
780}
781
782impl Rule for MD032BlanksAroundLists {
783 fn name(&self) -> &'static str {
784 "MD032"
785 }
786
787 fn description(&self) -> &'static str {
788 "Lists should be surrounded by blank lines"
789 }
790
791 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
792 let lines = ctx.raw_lines();
793 let line_index = &ctx.line_index;
794
795 if lines.is_empty() {
797 return Ok(Vec::new());
798 }
799
800 let list_blocks = self.convert_list_blocks(ctx);
801
802 if list_blocks.is_empty() {
803 return Ok(Vec::new());
804 }
805
806 let mut warnings = self.perform_checks(ctx, lines, &list_blocks, line_index);
807
808 if !self.config.allow_lazy_continuation {
813 let lazy_cont_lines = ctx.lazy_continuation_lines();
814
815 for lazy_info in lazy_cont_lines.iter() {
816 let line_num = lazy_info.line_num;
817
818 let is_within_block = list_blocks
822 .iter()
823 .any(|(start, end, _)| line_num >= *start && line_num <= *end);
824
825 if !is_within_block {
826 continue;
827 }
828
829 let line_content = lines.get(line_num.saturating_sub(1)).unwrap_or(&"");
831 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_num, line_content);
832
833 let fix = if Self::should_apply_lazy_fix(ctx, line_num) {
835 Self::calculate_lazy_continuation_fix(ctx, line_num, lazy_info)
836 } else {
837 None
838 };
839
840 warnings.push(LintWarning {
841 line: start_line,
842 column: start_col,
843 end_line,
844 end_column: end_col,
845 severity: Severity::Warning,
846 rule_name: Some(self.name().to_string()),
847 message: "Lazy continuation line should be properly indented or preceded by blank line".to_string(),
848 fix,
849 });
850 }
851 }
852
853 Ok(warnings)
854 }
855
856 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
857 Ok(self.fix_with_structure_impl(ctx))
858 }
859
860 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
861 ctx.content.is_empty() || ctx.list_blocks.is_empty()
864 }
865
866 fn category(&self) -> RuleCategory {
867 RuleCategory::List
868 }
869
870 fn as_any(&self) -> &dyn std::any::Any {
871 self
872 }
873
874 crate::impl_rule_config_methods!(MD032Config);
875}
876
877impl MD032BlanksAroundLists {
878 fn fix_with_structure_impl(&self, ctx: &crate::lint_context::LintContext) -> String {
880 let lines = ctx.raw_lines();
881 let num_lines = lines.len();
882 if num_lines == 0 {
883 return String::new();
884 }
885
886 let list_blocks = self.convert_list_blocks(ctx);
887 if list_blocks.is_empty() {
888 return ctx.content.to_string();
889 }
890
891 let mut lazy_fixes: std::collections::BTreeMap<usize, LazyContLine> = std::collections::BTreeMap::new();
894 if !self.config.allow_lazy_continuation {
895 let lazy_cont_lines = ctx.lazy_continuation_lines();
896 for lazy_info in lazy_cont_lines.iter() {
897 let line_num = lazy_info.line_num;
898 let is_within_block = list_blocks
900 .iter()
901 .any(|(start, end, _)| line_num >= *start && line_num <= *end);
902 if !is_within_block {
903 continue;
904 }
905 if !Self::should_apply_lazy_fix(ctx, line_num) {
907 continue;
908 }
909 lazy_fixes.insert(line_num, lazy_info.clone());
910 }
911 }
912
913 let mut insertions: std::collections::BTreeMap<usize, String> = std::collections::BTreeMap::new();
914
915 for &(start_line, end_line, ref prefix) in &list_blocks {
917 if ctx.inline_config().is_rule_disabled("MD032", start_line) {
919 continue;
920 }
921
922 if ctx
924 .line_info(start_line)
925 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
926 {
927 continue;
928 }
929
930 if start_line > 1 {
932 let (content_line, has_blank_separation) = Self::find_preceding_content(ctx, start_line);
934
935 if !has_blank_separation && content_line > 0 {
937 let prev_line_str = lines[content_line - 1];
938 let is_prev_excluded = ctx
939 .line_info(content_line)
940 .is_some_and(|info| info.in_code_block || info.in_front_matter);
941 let prev_prefix = BLOCKQUOTE_PREFIX_RE.find(prev_line_str).map_or("", |m| m.as_str());
942
943 let should_require = Self::should_require_blank_line_before(ctx, content_line, start_line);
944 if !is_prev_excluded && prev_prefix.trim() == prefix.trim() && should_require {
946 let bq_prefix = ctx.blockquote_prefix_for_blank_line(start_line - 1);
948 insertions.insert(start_line, bq_prefix);
949 }
950 }
951 }
952
953 if end_line < num_lines {
955 let (content_line, has_blank_separation) = Self::find_following_content(ctx, end_line);
957
958 if !has_blank_separation && content_line > 0 {
960 let next_line_str = lines[content_line - 1];
961 let is_next_excluded = ctx
963 .line_info(content_line)
964 .is_some_and(|info| info.in_code_block || info.in_front_matter)
965 || (content_line <= ctx.lines.len()
966 && ctx.lines[content_line - 1].in_code_block
967 && ctx.lines[content_line - 1].indent >= 2
968 && (ctx.lines[content_line - 1]
969 .content(ctx.content)
970 .trim()
971 .starts_with("```")
972 || ctx.lines[content_line - 1]
973 .content(ctx.content)
974 .trim()
975 .starts_with("~~~")));
976 let next_prefix = BLOCKQUOTE_PREFIX_RE.find(next_line_str).map_or("", |m| m.as_str());
977
978 let end_line_str = lines[end_line - 1];
980 let end_line_prefix = BLOCKQUOTE_PREFIX_RE.find(end_line_str).map_or("", |m| m.as_str());
981 let end_line_bq_level = end_line_prefix.chars().filter(|&c| c == '>').count();
982 let next_line_bq_level = next_prefix.chars().filter(|&c| c == '>').count();
983 let exits_blockquote = end_line_bq_level > 0 && next_line_bq_level < end_line_bq_level;
984
985 if !is_next_excluded && next_prefix.trim() == prefix.trim() && !exits_blockquote {
988 let bq_prefix = ctx.blockquote_prefix_for_blank_line(end_line - 1);
990 insertions.insert(end_line + 1, bq_prefix);
991 }
992 }
993 }
994 }
995
996 let mut result_lines: Vec<String> = Vec::with_capacity(num_lines + insertions.len());
998 for (i, line) in lines.iter().enumerate() {
999 let current_line_num = i + 1;
1000 if let Some(prefix_to_insert) = insertions.get(¤t_line_num)
1001 && (result_lines.is_empty() || result_lines.last().unwrap() != prefix_to_insert)
1002 {
1003 result_lines.push(prefix_to_insert.clone());
1004 }
1005
1006 if let Some(lazy_info) = lazy_fixes.get(¤t_line_num)
1008 && !ctx.inline_config().is_rule_disabled("MD032", current_line_num)
1009 {
1010 let fixed_line = Self::apply_lazy_fix_to_line(line, lazy_info);
1011 result_lines.push(fixed_line);
1012 } else {
1013 result_lines.push(line.to_string());
1014 }
1015 }
1016
1017 let mut result = result_lines.join("\n");
1019 if ctx.content.ends_with('\n') {
1020 result.push('\n');
1021 }
1022 result
1023 }
1024}
1025
1026fn is_blank_in_context(line: &str) -> bool {
1028 if let Some(m) = BLOCKQUOTE_PREFIX_RE.find(line) {
1031 line[m.end()..].trim().is_empty()
1033 } else {
1034 line.trim().is_empty()
1036 }
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041 use super::*;
1042 use crate::lint_context::LintContext;
1043 use crate::rule::Rule;
1044
1045 fn lint(content: &str) -> Vec<LintWarning> {
1046 let rule = MD032BlanksAroundLists::default();
1047 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1048 rule.check(&ctx).expect("Lint check failed")
1049 }
1050
1051 fn fix(content: &str) -> String {
1052 let rule = MD032BlanksAroundLists::default();
1053 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1054 rule.fix(&ctx).expect("Lint fix failed")
1055 }
1056
1057 #[test]
1058 fn test_fix_does_not_split_item_before_different_list_type() {
1059 let content = "- alpha beta\n aligned\n1. ordered item\n cont\n";
1063 assert_eq!(fix(content), "- alpha beta\n aligned\n\n1. ordered item\n cont\n");
1064
1065 let warnings = lint(content);
1068 assert_eq!(warnings.len(), 2);
1069 assert_eq!(warnings[0].line, 2);
1070 assert_eq!(warnings[1].line, 3);
1071 }
1072
1073 #[test]
1074 fn test_fix_does_not_split_blockquoted_item_before_different_list_type() {
1075 let content = "> - alpha beta\n> aligned\n> 1. ordered item\n";
1076 assert_eq!(fix(content), "> - alpha beta\n> aligned\n>\n> 1. ordered item\n");
1077 }
1078
1079 #[test]
1080 fn test_fix_keeps_lazy_continuation_with_its_item() {
1081 let content = "- alpha beta\nlazy\n1. ordered item\n";
1085 assert_eq!(fix(content), "- alpha beta\nlazy\n\n1. ordered item\n");
1086
1087 let warnings = lint(content);
1088 assert_eq!(warnings.len(), 2);
1089 assert_eq!(warnings[0].line, 2);
1090 assert_eq!(warnings[1].line, 3);
1091 }
1092
1093 #[test]
1094 fn test_fix_keeps_blockquoted_lazy_continuation_with_its_item() {
1095 let content = "> - alpha beta\n> lazy\n> 1. ordered item\n";
1096 assert_eq!(fix(content), "> - alpha beta\n> lazy\n>\n> 1. ordered item\n");
1097 }
1098
1099 #[test]
1100 fn test_fix_indents_lazy_continuation_when_not_allowed() {
1101 let rule = MD032BlanksAroundLists::from_config_struct(MD032Config {
1104 allow_lazy_continuation: false,
1105 });
1106 let content = "- alpha beta\nlazy\n1. ordered item\n";
1107 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1108 let fixed = rule.fix(&ctx).expect("Lint fix failed");
1109 assert_eq!(fixed, "- alpha beta\n lazy\n\n1. ordered item\n");
1110 }
1111
1112 fn check_warnings_have_fixes(content: &str) {
1114 let warnings = lint(content);
1115 for warning in &warnings {
1116 assert!(warning.fix.is_some(), "Warning should have fix: {warning:?}");
1117 }
1118 }
1119
1120 #[test]
1121 fn test_list_at_start() {
1122 let content = "- Item 1\n- Item 2\nText";
1125 let warnings = lint(content);
1126 assert_eq!(
1127 warnings.len(),
1128 0,
1129 "Trailing text is lazy continuation per CommonMark - no warning expected"
1130 );
1131 }
1132
1133 #[test]
1134 fn test_list_at_end() {
1135 let content = "Text\n- Item 1\n- Item 2";
1136 let warnings = lint(content);
1137 assert_eq!(
1138 warnings.len(),
1139 1,
1140 "Expected 1 warning for list at end without preceding blank line"
1141 );
1142 assert_eq!(
1143 warnings[0].line, 2,
1144 "Warning should be on the first line of the list (line 2)"
1145 );
1146 assert!(warnings[0].message.contains("preceded by blank line"));
1147
1148 check_warnings_have_fixes(content);
1150
1151 let fixed_content = fix(content);
1152 assert_eq!(fixed_content, "Text\n\n- Item 1\n- Item 2");
1153
1154 let warnings_after_fix = lint(&fixed_content);
1156 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1157 }
1158
1159 #[test]
1160 fn test_list_in_middle() {
1161 let content = "Text 1\n- Item 1\n- Item 2\nText 2";
1164 let warnings = lint(content);
1165 assert_eq!(
1166 warnings.len(),
1167 1,
1168 "Expected 1 warning for list needing preceding blank line (trailing text is lazy continuation)"
1169 );
1170 assert_eq!(warnings[0].line, 2, "Warning on line 2 (start)");
1171 assert!(warnings[0].message.contains("preceded by blank line"));
1172
1173 check_warnings_have_fixes(content);
1175
1176 let fixed_content = fix(content);
1177 assert_eq!(fixed_content, "Text 1\n\n- Item 1\n- Item 2\nText 2");
1178
1179 let warnings_after_fix = lint(&fixed_content);
1181 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1182 }
1183
1184 #[test]
1185 fn test_correct_spacing() {
1186 let content = "Text 1\n\n- Item 1\n- Item 2\n\nText 2";
1187 let warnings = lint(content);
1188 assert_eq!(warnings.len(), 0, "Expected no warnings for correctly spaced list");
1189
1190 let fixed_content = fix(content);
1191 assert_eq!(fixed_content, content, "Fix should not change correctly spaced content");
1192 }
1193
1194 #[test]
1195 fn test_list_with_content() {
1196 let content = "Text\n* Item 1\n Content\n* Item 2\n More content\nText";
1199 let warnings = lint(content);
1200 assert_eq!(
1201 warnings.len(),
1202 1,
1203 "Expected 1 warning for list needing preceding blank line. Got: {warnings:?}"
1204 );
1205 assert_eq!(warnings[0].line, 2, "Warning should be on line 2 (start)");
1206 assert!(warnings[0].message.contains("preceded by blank line"));
1207
1208 check_warnings_have_fixes(content);
1210
1211 let fixed_content = fix(content);
1212 let expected_fixed = "Text\n\n* Item 1\n Content\n* Item 2\n More content\nText";
1213 assert_eq!(
1214 fixed_content, expected_fixed,
1215 "Fix did not produce the expected output. Got:\n{fixed_content}"
1216 );
1217
1218 let warnings_after_fix = lint(&fixed_content);
1220 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1221 }
1222
1223 #[test]
1224 fn test_nested_list() {
1225 let content = "Text\n- Item 1\n - Nested 1\n- Item 2\nText";
1227 let warnings = lint(content);
1228 assert_eq!(
1229 warnings.len(),
1230 1,
1231 "Nested list block needs preceding blank only. Got: {warnings:?}"
1232 );
1233 assert_eq!(warnings[0].line, 2);
1234 assert!(warnings[0].message.contains("preceded by blank line"));
1235
1236 check_warnings_have_fixes(content);
1238
1239 let fixed_content = fix(content);
1240 assert_eq!(fixed_content, "Text\n\n- Item 1\n - Nested 1\n- Item 2\nText");
1241
1242 let warnings_after_fix = lint(&fixed_content);
1244 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1245 }
1246
1247 #[test]
1248 fn test_list_with_internal_blanks() {
1249 let content = "Text\n* Item 1\n\n More Item 1 Content\n* Item 2\nText";
1251 let warnings = lint(content);
1252 assert_eq!(
1253 warnings.len(),
1254 1,
1255 "List with internal blanks needs preceding blank only. Got: {warnings:?}"
1256 );
1257 assert_eq!(warnings[0].line, 2);
1258 assert!(warnings[0].message.contains("preceded by blank line"));
1259
1260 check_warnings_have_fixes(content);
1262
1263 let fixed_content = fix(content);
1264 assert_eq!(
1265 fixed_content,
1266 "Text\n\n* Item 1\n\n More Item 1 Content\n* Item 2\nText"
1267 );
1268
1269 let warnings_after_fix = lint(&fixed_content);
1271 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1272 }
1273
1274 #[test]
1275 fn test_ignore_code_blocks() {
1276 let content = "```\n- Not a list item\n```\nText";
1277 let warnings = lint(content);
1278 assert_eq!(warnings.len(), 0);
1279 let fixed_content = fix(content);
1280 assert_eq!(fixed_content, content);
1281 }
1282
1283 #[test]
1284 fn test_ignore_front_matter() {
1285 let content = "---\ntitle: Test\n---\n- List Item\nText";
1287 let warnings = lint(content);
1288 assert_eq!(
1289 warnings.len(),
1290 0,
1291 "Front matter test should have no MD032 warnings. Got: {warnings:?}"
1292 );
1293
1294 let fixed_content = fix(content);
1296 assert_eq!(fixed_content, content, "No changes when no warnings");
1297 }
1298
1299 #[test]
1300 fn test_multiple_lists() {
1301 let content = "Text\n- List 1 Item 1\n- List 1 Item 2\nText 2\n* List 2 Item 1\nText 3";
1306 let warnings = lint(content);
1307 assert!(
1309 !warnings.is_empty(),
1310 "Should have at least one warning for missing blank line. Got: {warnings:?}"
1311 );
1312
1313 check_warnings_have_fixes(content);
1315
1316 let fixed_content = fix(content);
1317 let warnings_after_fix = lint(&fixed_content);
1319 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1320 }
1321
1322 #[test]
1323 fn test_adjacent_lists() {
1324 let content = "- List 1\n\n* List 2";
1325 let warnings = lint(content);
1326 assert_eq!(warnings.len(), 0);
1327 let fixed_content = fix(content);
1328 assert_eq!(fixed_content, content);
1329 }
1330
1331 #[test]
1332 fn test_list_in_blockquote() {
1333 let content = "> Quote line 1\n> - List item 1\n> - List item 2\n> Quote line 2";
1335 let warnings = lint(content);
1336 assert_eq!(
1337 warnings.len(),
1338 1,
1339 "Expected 1 warning for blockquoted list needing preceding blank. Got: {warnings:?}"
1340 );
1341 assert_eq!(warnings[0].line, 2);
1342
1343 check_warnings_have_fixes(content);
1345
1346 let fixed_content = fix(content);
1347 assert_eq!(
1349 fixed_content, "> Quote line 1\n>\n> - List item 1\n> - List item 2\n> Quote line 2",
1350 "Fix for blockquoted list failed. Got:\n{fixed_content}"
1351 );
1352
1353 let warnings_after_fix = lint(&fixed_content);
1355 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1356 }
1357
1358 #[test]
1359 fn test_ordered_list() {
1360 let content = "Text\n1. Item 1\n2. Item 2\nText";
1362 let warnings = lint(content);
1363 assert_eq!(warnings.len(), 1);
1364
1365 check_warnings_have_fixes(content);
1367
1368 let fixed_content = fix(content);
1369 assert_eq!(fixed_content, "Text\n\n1. Item 1\n2. Item 2\nText");
1370
1371 let warnings_after_fix = lint(&fixed_content);
1373 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1374 }
1375
1376 #[test]
1377 fn test_no_double_blank_fix() {
1378 let content = "Text\n\n- Item 1\n- Item 2\nText"; let warnings = lint(content);
1381 assert_eq!(
1382 warnings.len(),
1383 0,
1384 "Should have no warnings - properly preceded, trailing is lazy"
1385 );
1386
1387 let fixed_content = fix(content);
1388 assert_eq!(
1389 fixed_content, content,
1390 "No fix needed when no warnings. Got:\n{fixed_content}"
1391 );
1392
1393 let content2 = "Text\n- Item 1\n- Item 2\n\nText"; let warnings2 = lint(content2);
1395 assert_eq!(warnings2.len(), 1);
1396 if !warnings2.is_empty() {
1397 assert_eq!(
1398 warnings2[0].line, 2,
1399 "Warning line for missing blank before should be the first line of the block"
1400 );
1401 }
1402
1403 check_warnings_have_fixes(content2);
1405
1406 let fixed_content2 = fix(content2);
1407 assert_eq!(
1408 fixed_content2, "Text\n\n- Item 1\n- Item 2\n\nText",
1409 "Fix added extra blank before. Got:\n{fixed_content2}"
1410 );
1411 }
1412
1413 #[test]
1414 fn test_empty_input() {
1415 let content = "";
1416 let warnings = lint(content);
1417 assert_eq!(warnings.len(), 0);
1418 let fixed_content = fix(content);
1419 assert_eq!(fixed_content, "");
1420 }
1421
1422 #[test]
1423 fn test_only_list() {
1424 let content = "- Item 1\n- Item 2";
1425 let warnings = lint(content);
1426 assert_eq!(warnings.len(), 0);
1427 let fixed_content = fix(content);
1428 assert_eq!(fixed_content, content);
1429 }
1430
1431 #[test]
1434 fn test_fix_complex_nested_blockquote() {
1435 let content = "> Text before\n> - Item 1\n> - Nested item\n> - Item 2\n> Text after";
1437 let warnings = lint(content);
1438 assert_eq!(
1439 warnings.len(),
1440 1,
1441 "Should warn for missing preceding blank only. Got: {warnings:?}"
1442 );
1443
1444 check_warnings_have_fixes(content);
1446
1447 let fixed_content = fix(content);
1448 let expected = "> Text before\n>\n> - Item 1\n> - Nested item\n> - Item 2\n> Text after";
1450 assert_eq!(fixed_content, expected, "Fix should preserve blockquote structure");
1451
1452 let warnings_after_fix = lint(&fixed_content);
1453 assert_eq!(warnings_after_fix.len(), 0, "Fix should eliminate all warnings");
1454 }
1455
1456 #[test]
1457 fn test_fix_mixed_list_markers() {
1458 let content = "Text\n- Item 1\n* Item 2\n+ Item 3\nText";
1461 let warnings = lint(content);
1462 assert!(
1464 !warnings.is_empty(),
1465 "Should have at least 1 warning for mixed marker list. Got: {warnings:?}"
1466 );
1467
1468 check_warnings_have_fixes(content);
1470
1471 let fixed_content = fix(content);
1472 assert!(
1474 fixed_content.contains("Text\n\n-"),
1475 "Fix should add blank line before first list item"
1476 );
1477
1478 let warnings_after_fix = lint(&fixed_content);
1480 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1481 }
1482
1483 #[test]
1484 fn test_fix_ordered_list_with_different_numbers() {
1485 let content = "Text\n1. First\n3. Third\n2. Second\nText";
1487 let warnings = lint(content);
1488 assert_eq!(warnings.len(), 1, "Should warn for missing preceding blank only");
1489
1490 check_warnings_have_fixes(content);
1492
1493 let fixed_content = fix(content);
1494 let expected = "Text\n\n1. First\n3. Third\n2. Second\nText";
1495 assert_eq!(
1496 fixed_content, expected,
1497 "Fix should handle ordered lists with non-sequential numbers"
1498 );
1499
1500 let warnings_after_fix = lint(&fixed_content);
1502 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1503 }
1504
1505 #[test]
1506 fn test_fix_list_with_code_blocks_inside() {
1507 let content = "Text\n- Item 1\n ```\n code\n ```\n- Item 2\nText";
1509 let warnings = lint(content);
1510 assert_eq!(warnings.len(), 1, "Should warn for missing preceding blank only");
1511
1512 check_warnings_have_fixes(content);
1514
1515 let fixed_content = fix(content);
1516 let expected = "Text\n\n- Item 1\n ```\n code\n ```\n- Item 2\nText";
1517 assert_eq!(
1518 fixed_content, expected,
1519 "Fix should handle lists with internal code blocks"
1520 );
1521
1522 let warnings_after_fix = lint(&fixed_content);
1524 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1525 }
1526
1527 #[test]
1528 fn test_fix_deeply_nested_lists() {
1529 let content = "Text\n- Level 1\n - Level 2\n - Level 3\n - Level 4\n- Back to Level 1\nText";
1531 let warnings = lint(content);
1532 assert_eq!(warnings.len(), 1, "Should warn for missing preceding blank only");
1533
1534 check_warnings_have_fixes(content);
1536
1537 let fixed_content = fix(content);
1538 let expected = "Text\n\n- Level 1\n - Level 2\n - Level 3\n - Level 4\n- Back to Level 1\nText";
1539 assert_eq!(fixed_content, expected, "Fix should handle deeply nested lists");
1540
1541 let warnings_after_fix = lint(&fixed_content);
1543 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1544 }
1545
1546 #[test]
1547 fn test_fix_list_with_multiline_items() {
1548 let content = "Text\n- Item 1\n continues here\n and here\n- Item 2\n also continues\nText";
1551 let warnings = lint(content);
1552 assert_eq!(
1553 warnings.len(),
1554 1,
1555 "Should only warn for missing blank before list (trailing text is lazy continuation)"
1556 );
1557
1558 check_warnings_have_fixes(content);
1560
1561 let fixed_content = fix(content);
1562 let expected = "Text\n\n- Item 1\n continues here\n and here\n- Item 2\n also continues\nText";
1563 assert_eq!(fixed_content, expected, "Fix should add blank before list only");
1564
1565 let warnings_after_fix = lint(&fixed_content);
1567 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1568 }
1569
1570 #[test]
1571 fn test_fix_list_at_document_boundaries() {
1572 let content1 = "- Item 1\n- Item 2";
1574 let warnings1 = lint(content1);
1575 assert_eq!(
1576 warnings1.len(),
1577 0,
1578 "List at document start should not need blank before"
1579 );
1580 let fixed1 = fix(content1);
1581 assert_eq!(fixed1, content1, "No fix needed for list at start");
1582
1583 let content2 = "Text\n- Item 1\n- Item 2";
1585 let warnings2 = lint(content2);
1586 assert_eq!(warnings2.len(), 1, "List at document end should need blank before");
1587 check_warnings_have_fixes(content2);
1588 let fixed2 = fix(content2);
1589 assert_eq!(
1590 fixed2, "Text\n\n- Item 1\n- Item 2",
1591 "Should add blank before list at end"
1592 );
1593 }
1594
1595 #[test]
1596 fn test_fix_preserves_existing_blank_lines() {
1597 let content = "Text\n\n\n- Item 1\n- Item 2\n\n\nText";
1598 let warnings = lint(content);
1599 assert_eq!(warnings.len(), 0, "Multiple blank lines should be preserved");
1600 let fixed_content = fix(content);
1601 assert_eq!(fixed_content, content, "Fix should not modify already correct content");
1602 }
1603
1604 #[test]
1605 fn test_fix_handles_tabs_and_spaces() {
1606 let content = "Text\n\t- Item with tab\n - Item with spaces\nText";
1609 let warnings = lint(content);
1610 assert!(!warnings.is_empty(), "Should warn for missing blank before list");
1612
1613 check_warnings_have_fixes(content);
1615
1616 let fixed_content = fix(content);
1617 let expected = "Text\n\t- Item with tab\n\n - Item with spaces\nText";
1620 assert_eq!(fixed_content, expected, "Fix should add blank before list item");
1621
1622 let warnings_after_fix = lint(&fixed_content);
1624 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1625 }
1626
1627 #[test]
1628 fn test_fix_warning_objects_have_correct_ranges() {
1629 let content = "Text\n- Item 1\n- Item 2\nText";
1631 let warnings = lint(content);
1632 assert_eq!(warnings.len(), 1, "Only preceding blank warning expected");
1633
1634 for warning in &warnings {
1636 assert!(warning.fix.is_some(), "Warning should have fix");
1637 let fix = warning.fix.as_ref().unwrap();
1638 assert!(fix.range.start <= fix.range.end, "Fix range should be valid");
1639 assert!(
1640 !fix.replacement.is_empty() || fix.range.start == fix.range.end,
1641 "Fix should have replacement or be insertion"
1642 );
1643 }
1644 }
1645
1646 #[test]
1647 fn test_fix_idempotent() {
1648 let content = "Text\n- Item 1\n- Item 2\nText";
1650
1651 let fixed_once = fix(content);
1653 assert_eq!(fixed_once, "Text\n\n- Item 1\n- Item 2\nText");
1654
1655 let fixed_twice = fix(&fixed_once);
1657 assert_eq!(fixed_twice, fixed_once, "Fix should be idempotent");
1658
1659 let warnings_after_fix = lint(&fixed_once);
1661 assert_eq!(warnings_after_fix.len(), 0, "No warnings should remain after fix");
1662 }
1663
1664 #[test]
1665 fn test_fix_with_normalized_line_endings() {
1666 let content = "Text\n- Item 1\n- Item 2\nText";
1670 let warnings = lint(content);
1671 assert_eq!(warnings.len(), 1, "Should detect missing blank before list");
1672
1673 check_warnings_have_fixes(content);
1675
1676 let fixed_content = fix(content);
1677 let expected = "Text\n\n- Item 1\n- Item 2\nText";
1679 assert_eq!(fixed_content, expected, "Fix should work with normalized LF content");
1680 }
1681
1682 #[test]
1683 fn test_fix_preserves_final_newline() {
1684 let content_with_newline = "Text\n- Item 1\n- Item 2\nText\n";
1687 let fixed_with_newline = fix(content_with_newline);
1688 assert!(
1689 fixed_with_newline.ends_with('\n'),
1690 "Fix should preserve final newline when present"
1691 );
1692 assert_eq!(fixed_with_newline, "Text\n\n- Item 1\n- Item 2\nText\n");
1694
1695 let content_without_newline = "Text\n- Item 1\n- Item 2\nText";
1697 let fixed_without_newline = fix(content_without_newline);
1698 assert!(
1699 !fixed_without_newline.ends_with('\n'),
1700 "Fix should not add final newline when not present"
1701 );
1702 assert_eq!(fixed_without_newline, "Text\n\n- Item 1\n- Item 2\nText");
1704 }
1705
1706 #[test]
1707 fn test_fix_multiline_list_items_no_indent() {
1708 let content = "## Configuration\n\nThis rule has the following configuration options:\n\n- `option1`: Description that continues\non the next line without indentation.\n- `option2`: Another description that also continues\non the next line.\n\n## Next Section";
1709
1710 let warnings = lint(content);
1711 assert_eq!(
1713 warnings.len(),
1714 0,
1715 "Should not warn for properly formatted list with multi-line items. Got: {warnings:?}"
1716 );
1717
1718 let fixed_content = fix(content);
1719 assert_eq!(
1721 fixed_content, content,
1722 "Should not modify correctly formatted multi-line list items"
1723 );
1724 }
1725
1726 #[test]
1727 fn test_nested_list_with_lazy_continuation() {
1728 let content = r#"# Test
1734
1735- **Token Dispatch (Phase 3.2)**: COMPLETE. Extracts tokens from both:
1736 1. Switch/case dispatcher statements (original Phase 3.2)
1737 2. Inline conditionals - if/else, bitwise checks (`&`, `|`), comparison (`==`,
1738`!=`), ternary operators (`?:`), macros (`ISTOK`, `ISUNSET`), compound conditions (`&&`, `||`) (Phase 3.2.1)
1739 - 30 explicit tokens extracted, 23 dispatcher rules with embedded token
1740 references"#;
1741
1742 let warnings = lint(content);
1743 let md032_warnings: Vec<_> = warnings
1746 .iter()
1747 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1748 .collect();
1749 assert_eq!(
1750 md032_warnings.len(),
1751 0,
1752 "Should not warn for nested list with lazy continuation. Got: {md032_warnings:?}"
1753 );
1754 }
1755
1756 #[test]
1757 fn test_pipes_in_code_spans_not_detected_as_table() {
1758 let content = r#"# Test
1760
1761- Item with `a | b` inline code
1762 - Nested item should work
1763
1764"#;
1765
1766 let warnings = lint(content);
1767 let md032_warnings: Vec<_> = warnings
1768 .iter()
1769 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1770 .collect();
1771 assert_eq!(
1772 md032_warnings.len(),
1773 0,
1774 "Pipes in code spans should not break lists. Got: {md032_warnings:?}"
1775 );
1776 }
1777
1778 #[test]
1779 fn test_multiple_code_spans_with_pipes() {
1780 let content = r#"# Test
1782
1783- Item with `a | b` and `c || d` operators
1784 - Nested item should work
1785
1786"#;
1787
1788 let warnings = lint(content);
1789 let md032_warnings: Vec<_> = warnings
1790 .iter()
1791 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1792 .collect();
1793 assert_eq!(
1794 md032_warnings.len(),
1795 0,
1796 "Multiple code spans with pipes should not break lists. Got: {md032_warnings:?}"
1797 );
1798 }
1799
1800 #[test]
1801 fn test_actual_table_breaks_list() {
1802 let content = r#"# Test
1804
1805- Item before table
1806
1807| Col1 | Col2 |
1808|------|------|
1809| A | B |
1810
1811- Item after table
1812
1813"#;
1814
1815 let warnings = lint(content);
1816 let md032_warnings: Vec<_> = warnings
1818 .iter()
1819 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1820 .collect();
1821 assert_eq!(
1822 md032_warnings.len(),
1823 0,
1824 "Both lists should be properly separated by blank lines. Got: {md032_warnings:?}"
1825 );
1826 }
1827
1828 #[test]
1829 fn test_thematic_break_not_lazy_continuation() {
1830 let content = r#"- Item 1
1833- Item 2
1834***
1835
1836More text.
1837"#;
1838
1839 let warnings = lint(content);
1840 let md032_warnings: Vec<_> = warnings
1841 .iter()
1842 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1843 .collect();
1844 assert_eq!(
1845 md032_warnings.len(),
1846 1,
1847 "Should warn for list not followed by blank line before thematic break. Got: {md032_warnings:?}"
1848 );
1849 assert!(
1850 md032_warnings[0].message.contains("followed by blank line"),
1851 "Warning should be about missing blank after list"
1852 );
1853 }
1854
1855 #[test]
1856 fn test_thematic_break_with_blank_line() {
1857 let content = r#"- Item 1
1859- Item 2
1860
1861***
1862
1863More text.
1864"#;
1865
1866 let warnings = lint(content);
1867 let md032_warnings: Vec<_> = warnings
1868 .iter()
1869 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1870 .collect();
1871 assert_eq!(
1872 md032_warnings.len(),
1873 0,
1874 "Should not warn when list is properly followed by blank line. Got: {md032_warnings:?}"
1875 );
1876 }
1877
1878 #[test]
1879 fn test_various_thematic_break_styles() {
1880 for hr in ["---", "***", "___"] {
1885 let content = format!(
1886 r#"- Item 1
1887- Item 2
1888{hr}
1889
1890More text.
1891"#
1892 );
1893
1894 let warnings = lint(&content);
1895 let md032_warnings: Vec<_> = warnings
1896 .iter()
1897 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1898 .collect();
1899 assert_eq!(
1900 md032_warnings.len(),
1901 1,
1902 "Should warn for HR style '{hr}' without blank line. Got: {md032_warnings:?}"
1903 );
1904 }
1905 }
1906
1907 fn lint_with_config(content: &str, config: MD032Config) -> Vec<LintWarning> {
1910 let rule = MD032BlanksAroundLists::from_config_struct(config);
1911 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1912 rule.check(&ctx).expect("Lint check failed")
1913 }
1914
1915 fn fix_with_config(content: &str, config: MD032Config) -> String {
1916 let rule = MD032BlanksAroundLists::from_config_struct(config);
1917 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1918 rule.fix(&ctx).expect("Lint fix failed")
1919 }
1920
1921 #[test]
1922 fn test_lazy_continuation_allowed_by_default() {
1923 let content = "# Heading\n\n1. List\nSome text.";
1925 let warnings = lint(content);
1926 assert_eq!(
1927 warnings.len(),
1928 0,
1929 "Default behavior should allow lazy continuation. Got: {warnings:?}"
1930 );
1931 }
1932
1933 #[test]
1934 fn test_lazy_continuation_disallowed() {
1935 let content = "# Heading\n\n1. List\nSome text.";
1937 let config = MD032Config {
1938 allow_lazy_continuation: false,
1939 };
1940 let warnings = lint_with_config(content, config);
1941 assert_eq!(
1942 warnings.len(),
1943 1,
1944 "Should warn when lazy continuation is disallowed. Got: {warnings:?}"
1945 );
1946 assert!(
1947 warnings[0].message.contains("Lazy continuation"),
1948 "Warning message should mention lazy continuation"
1949 );
1950 assert_eq!(warnings[0].line, 4, "Warning should be on the lazy line");
1951 }
1952
1953 #[test]
1954 fn test_lazy_continuation_fix() {
1955 let content = "# Heading\n\n1. List\nSome text.";
1957 let config = MD032Config {
1958 allow_lazy_continuation: false,
1959 };
1960 let fixed = fix_with_config(content, config.clone());
1961 assert_eq!(
1963 fixed, "# Heading\n\n1. List\n Some text.",
1964 "Fix should add proper indentation to lazy continuation"
1965 );
1966
1967 let warnings_after = lint_with_config(&fixed, config);
1969 assert_eq!(warnings_after.len(), 0, "No warnings should remain after fix");
1970 }
1971
1972 #[test]
1973 fn test_lazy_continuation_multiple_lines() {
1974 let content = "- Item 1\nLine 2\nLine 3";
1976 let config = MD032Config {
1977 allow_lazy_continuation: false,
1978 };
1979 let warnings = lint_with_config(content, config.clone());
1980 assert_eq!(
1982 warnings.len(),
1983 2,
1984 "Should warn for each lazy continuation line. Got: {warnings:?}"
1985 );
1986
1987 let fixed = fix_with_config(content, config.clone());
1988 assert_eq!(
1990 fixed, "- Item 1\n Line 2\n Line 3",
1991 "Fix should add proper indentation to lazy continuation lines"
1992 );
1993
1994 let warnings_after = lint_with_config(&fixed, config);
1996 assert_eq!(warnings_after.len(), 0, "No warnings should remain after fix");
1997 }
1998
1999 #[test]
2000 fn test_lazy_continuation_with_indented_content() {
2001 let content = "- Item 1\n Indented content\nLazy text";
2003 let config = MD032Config {
2004 allow_lazy_continuation: false,
2005 };
2006 let warnings = lint_with_config(content, config);
2007 assert_eq!(
2008 warnings.len(),
2009 1,
2010 "Should warn for lazy text after indented content. Got: {warnings:?}"
2011 );
2012 }
2013
2014 #[test]
2015 fn test_lazy_continuation_properly_separated() {
2016 let content = "- Item 1\n\nSome text.";
2018 let config = MD032Config {
2019 allow_lazy_continuation: false,
2020 };
2021 let warnings = lint_with_config(content, config);
2022 assert_eq!(
2023 warnings.len(),
2024 0,
2025 "Should not warn when list is properly followed by blank line. Got: {warnings:?}"
2026 );
2027 }
2028
2029 #[test]
2032 fn test_lazy_continuation_ordered_list_parenthesis_marker() {
2033 let content = "1) First item\nLazy continuation";
2035 let config = MD032Config {
2036 allow_lazy_continuation: false,
2037 };
2038 let warnings = lint_with_config(content, config.clone());
2039 assert_eq!(
2040 warnings.len(),
2041 1,
2042 "Should warn for lazy continuation with parenthesis marker"
2043 );
2044
2045 let fixed = fix_with_config(content, config);
2046 assert_eq!(fixed, "1) First item\n Lazy continuation");
2048 }
2049
2050 #[test]
2051 fn test_lazy_continuation_followed_by_another_list() {
2052 let content = "- Item 1\nSome text\n- Item 2";
2058 let config = MD032Config {
2059 allow_lazy_continuation: false,
2060 };
2061 let warnings = lint_with_config(content, config);
2062 assert_eq!(
2064 warnings.len(),
2065 1,
2066 "Should warn about lazy continuation within list. Got: {warnings:?}"
2067 );
2068 assert!(
2069 warnings[0].message.contains("Lazy continuation"),
2070 "Warning should be about lazy continuation"
2071 );
2072 assert_eq!(warnings[0].line, 2, "Warning should be on line 2");
2073 }
2074
2075 #[test]
2076 fn test_lazy_continuation_multiple_in_document() {
2077 let content = "- Item 1\nLazy 1\n\n- Item 2\nLazy 2";
2082 let config = MD032Config {
2083 allow_lazy_continuation: false,
2084 };
2085 let warnings = lint_with_config(content, config.clone());
2086 assert_eq!(
2088 warnings.len(),
2089 2,
2090 "Should warn for both lazy continuations. Got: {warnings:?}"
2091 );
2092
2093 let fixed = fix_with_config(content, config.clone());
2094 assert!(
2096 fixed.contains(" Lazy 1"),
2097 "Fixed content should have indented 'Lazy 1'. Got: {fixed:?}"
2098 );
2099 assert!(
2100 fixed.contains(" Lazy 2"),
2101 "Fixed content should have indented 'Lazy 2'. Got: {fixed:?}"
2102 );
2103
2104 let warnings_after = lint_with_config(&fixed, config);
2105 assert_eq!(
2107 warnings_after.len(),
2108 0,
2109 "All warnings should be fixed after auto-fix. Got: {warnings_after:?}"
2110 );
2111 }
2112
2113 #[test]
2114 fn test_lazy_continuation_end_of_document_no_newline() {
2115 let content = "- Item\nNo trailing newline";
2117 let config = MD032Config {
2118 allow_lazy_continuation: false,
2119 };
2120 let warnings = lint_with_config(content, config.clone());
2121 assert_eq!(warnings.len(), 1, "Should warn even at end of document");
2122
2123 let fixed = fix_with_config(content, config);
2124 assert_eq!(fixed, "- Item\n No trailing newline");
2126 }
2127
2128 #[test]
2129 fn test_lazy_continuation_thematic_break_still_needs_blank() {
2130 let content = "- Item 1\n---";
2133 let config = MD032Config {
2134 allow_lazy_continuation: false,
2135 };
2136 let warnings = lint_with_config(content, config.clone());
2137 assert_eq!(
2139 warnings.len(),
2140 1,
2141 "List should need blank line before thematic break. Got: {warnings:?}"
2142 );
2143
2144 let fixed = fix_with_config(content, config);
2146 assert_eq!(fixed, "- Item 1\n\n---");
2147 }
2148
2149 #[test]
2150 fn test_lazy_continuation_heading_not_flagged() {
2151 let content = "- Item 1\n# Heading";
2154 let config = MD032Config {
2155 allow_lazy_continuation: false,
2156 };
2157 let warnings = lint_with_config(content, config);
2158 assert!(
2161 warnings.iter().all(|w| !w.message.contains("lazy")),
2162 "Heading should not trigger lazy continuation warning"
2163 );
2164 }
2165
2166 #[test]
2167 fn test_lazy_continuation_mixed_list_types() {
2168 let content = "- Unordered\n1. Ordered\nLazy text";
2170 let config = MD032Config {
2171 allow_lazy_continuation: false,
2172 };
2173 let warnings = lint_with_config(content, config.clone());
2174 assert!(!warnings.is_empty(), "Should warn about structure issues");
2175 }
2176
2177 #[test]
2178 fn test_lazy_continuation_deep_nesting() {
2179 let content = "- Level 1\n - Level 2\n - Level 3\nLazy at root";
2181 let config = MD032Config {
2182 allow_lazy_continuation: false,
2183 };
2184 let warnings = lint_with_config(content, config.clone());
2185 assert!(
2186 !warnings.is_empty(),
2187 "Should warn about lazy continuation after nested list"
2188 );
2189
2190 let fixed = fix_with_config(content, config.clone());
2191 let warnings_after = lint_with_config(&fixed, config);
2192 assert_eq!(warnings_after.len(), 0, "No warnings should remain after fix");
2193 }
2194
2195 #[test]
2196 fn test_lazy_continuation_with_emphasis_in_text() {
2197 let content = "- Item\n*emphasized* continuation";
2199 let config = MD032Config {
2200 allow_lazy_continuation: false,
2201 };
2202 let warnings = lint_with_config(content, config.clone());
2203 assert_eq!(warnings.len(), 1, "Should warn even with emphasis in continuation");
2204
2205 let fixed = fix_with_config(content, config);
2206 assert_eq!(fixed, "- Item\n *emphasized* continuation");
2208 }
2209
2210 #[test]
2211 fn test_lazy_continuation_with_code_span() {
2212 let content = "- Item\n`code` continuation";
2214 let config = MD032Config {
2215 allow_lazy_continuation: false,
2216 };
2217 let warnings = lint_with_config(content, config.clone());
2218 assert_eq!(warnings.len(), 1, "Should warn even with code span in continuation");
2219
2220 let fixed = fix_with_config(content, config);
2221 assert_eq!(fixed, "- Item\n `code` continuation");
2223 }
2224
2225 #[test]
2232 fn test_issue295_case1_nested_bullets_then_continuation_then_item() {
2233 let content = r#"1. Create a new Chat conversation:
2236 - On the sidebar, select **New Chat**.
2237 - In the box, type `/new`.
2238 A new Chat conversation replaces the previous one.
22391. Under the Chat text box, turn off the toggle."#;
2240 let config = MD032Config {
2241 allow_lazy_continuation: false,
2242 };
2243 let warnings = lint_with_config(content, config);
2244 let lazy_warnings: Vec<_> = warnings
2246 .iter()
2247 .filter(|w| w.message.contains("Lazy continuation"))
2248 .collect();
2249 assert!(
2250 !lazy_warnings.is_empty(),
2251 "Should detect lazy continuation after nested bullets. Got: {warnings:?}"
2252 );
2253 assert!(
2254 lazy_warnings.iter().any(|w| w.line == 4),
2255 "Should warn on line 4. Got: {lazy_warnings:?}"
2256 );
2257 }
2258
2259 #[test]
2260 fn test_issue295_case3_code_span_starts_lazy_continuation() {
2261 let content = r#"- `field`: Is the specific key:
2264 - `password`: Accesses the password.
2265 - `api_key`: Accesses the api_key.
2266 `token`: Specifies which ID token to use.
2267- `version_id`: Is the unique identifier."#;
2268 let config = MD032Config {
2269 allow_lazy_continuation: false,
2270 };
2271 let warnings = lint_with_config(content, config);
2272 let lazy_warnings: Vec<_> = warnings
2274 .iter()
2275 .filter(|w| w.message.contains("Lazy continuation"))
2276 .collect();
2277 assert!(
2278 !lazy_warnings.is_empty(),
2279 "Should detect lazy continuation starting with code span. Got: {warnings:?}"
2280 );
2281 assert!(
2282 lazy_warnings.iter().any(|w| w.line == 4),
2283 "Should warn on line 4 (code span start). Got: {lazy_warnings:?}"
2284 );
2285 }
2286
2287 #[test]
2288 fn test_issue295_case4_deep_nesting_with_continuation_then_item() {
2289 let content = r#"- Check out the branch, and test locally.
2291 - If the MR requires significant modifications:
2292 - **Skip local testing** and review instead.
2293 - **Request verification** from the author.
2294 - **Identify the minimal change** needed.
2295 Your testing might result in opportunities.
2296- If you don't understand, _say so_."#;
2297 let config = MD032Config {
2298 allow_lazy_continuation: false,
2299 };
2300 let warnings = lint_with_config(content, config);
2301 let lazy_warnings: Vec<_> = warnings
2303 .iter()
2304 .filter(|w| w.message.contains("Lazy continuation"))
2305 .collect();
2306 assert!(
2307 !lazy_warnings.is_empty(),
2308 "Should detect lazy continuation after deep nesting. Got: {warnings:?}"
2309 );
2310 assert!(
2311 lazy_warnings.iter().any(|w| w.line == 6),
2312 "Should warn on line 6. Got: {lazy_warnings:?}"
2313 );
2314 }
2315
2316 #[test]
2317 fn test_issue295_ordered_list_nested_bullets_continuation() {
2318 let content = r#"# Test
2321
23221. First item.
2323 - Nested A.
2324 - Nested B.
2325 Continuation at outer level.
23261. Second item."#;
2327 let config = MD032Config {
2328 allow_lazy_continuation: false,
2329 };
2330 let warnings = lint_with_config(content, config);
2331 let lazy_warnings: Vec<_> = warnings
2333 .iter()
2334 .filter(|w| w.message.contains("Lazy continuation"))
2335 .collect();
2336 assert!(
2337 !lazy_warnings.is_empty(),
2338 "Should detect lazy continuation at outer level after nested. Got: {warnings:?}"
2339 );
2340 assert!(
2342 lazy_warnings.iter().any(|w| w.line == 6),
2343 "Should warn on line 6. Got: {lazy_warnings:?}"
2344 );
2345 }
2346
2347 #[test]
2348 fn test_issue295_multiple_lazy_lines_after_nested() {
2349 let content = r#"1. The device client receives a response.
2351 - Those defined by OAuth Framework.
2352 - Those specific to device authorization.
2353 Those error responses are described below.
2354 For more information on each response,
2355 see the documentation.
23561. Next step in the process."#;
2357 let config = MD032Config {
2358 allow_lazy_continuation: false,
2359 };
2360 let warnings = lint_with_config(content, config);
2361 let lazy_warnings: Vec<_> = warnings
2363 .iter()
2364 .filter(|w| w.message.contains("Lazy continuation"))
2365 .collect();
2366 assert!(
2367 lazy_warnings.len() >= 3,
2368 "Should detect multiple lazy continuation lines. Got {} warnings: {lazy_warnings:?}",
2369 lazy_warnings.len()
2370 );
2371 }
2372
2373 #[test]
2374 fn test_issue295_properly_indented_not_lazy() {
2375 let content = r#"1. First item.
2377 - Nested A.
2378 - Nested B.
2379
2380 Properly indented continuation.
23811. Second item."#;
2382 let config = MD032Config {
2383 allow_lazy_continuation: false,
2384 };
2385 let warnings = lint_with_config(content, config);
2386 let lazy_warnings: Vec<_> = warnings
2388 .iter()
2389 .filter(|w| w.message.contains("Lazy continuation"))
2390 .collect();
2391 assert_eq!(
2392 lazy_warnings.len(),
2393 0,
2394 "Should NOT warn when blank line separates continuation. Got: {lazy_warnings:?}"
2395 );
2396 }
2397
2398 #[test]
2405 fn test_html_comment_before_list_with_preceding_blank() {
2406 let content = "Some text.\n\n<!-- comment -->\n- List item";
2409 let warnings = lint(content);
2410 assert_eq!(
2411 warnings.len(),
2412 0,
2413 "Should not warn when blank line exists before HTML comment. Got: {warnings:?}"
2414 );
2415 }
2416
2417 #[test]
2418 fn test_html_comment_after_list_with_following_blank() {
2419 let content = "- List item\n<!-- comment -->\n\nSome text.";
2421 let warnings = lint(content);
2422 assert_eq!(
2423 warnings.len(),
2424 0,
2425 "Should not warn when blank line exists after HTML comment. Got: {warnings:?}"
2426 );
2427 }
2428
2429 #[test]
2430 fn test_list_inside_html_comment_ignored() {
2431 let content = "<!--\n1. First\n2. Second\n3. Third\n-->";
2433 let warnings = lint(content);
2434 assert_eq!(
2435 warnings.len(),
2436 0,
2437 "Should not analyze lists inside HTML comments. Got: {warnings:?}"
2438 );
2439 }
2440
2441 #[test]
2442 fn test_multiline_html_comment_before_list() {
2443 let content = "Text\n\n<!--\nThis is a\nmulti-line\ncomment\n-->\n- Item";
2445 let warnings = lint(content);
2446 assert_eq!(
2447 warnings.len(),
2448 0,
2449 "Multi-line HTML comment should be transparent. Got: {warnings:?}"
2450 );
2451 }
2452
2453 #[test]
2454 fn test_no_blank_before_html_comment_still_warns() {
2455 let content = "Some text.\n<!-- comment -->\n- List item";
2457 let warnings = lint(content);
2458 assert_eq!(
2459 warnings.len(),
2460 1,
2461 "Should warn when no blank line exists (even with HTML comment). Got: {warnings:?}"
2462 );
2463 assert!(
2464 warnings[0].message.contains("preceded by blank line"),
2465 "Should be 'preceded by blank line' warning"
2466 );
2467 }
2468
2469 #[test]
2470 fn test_no_blank_after_html_comment_no_warn_lazy_continuation() {
2471 let content = "- List item\n<!-- comment -->\nSome text.";
2474 let warnings = lint(content);
2475 assert_eq!(
2476 warnings.len(),
2477 0,
2478 "Should not warn - text after comment becomes lazy continuation. Got: {warnings:?}"
2479 );
2480 }
2481
2482 #[test]
2483 fn test_list_followed_by_heading_through_comment_should_warn() {
2484 let content = "- List item\n<!-- comment -->\n# Heading";
2486 let warnings = lint(content);
2487 assert!(
2490 warnings.len() <= 1,
2491 "Should handle heading after comment gracefully. Got: {warnings:?}"
2492 );
2493 }
2494
2495 #[test]
2496 fn test_html_comment_between_list_and_text_both_directions() {
2497 let content = "Text before.\n\n<!-- comment -->\n- Item 1\n- Item 2\n<!-- another -->\n\nText after.";
2499 let warnings = lint(content);
2500 assert_eq!(
2501 warnings.len(),
2502 0,
2503 "Should not warn with proper separation through comments. Got: {warnings:?}"
2504 );
2505 }
2506
2507 #[test]
2508 fn test_html_comment_fix_does_not_insert_unnecessary_blank() {
2509 let content = "Text.\n\n<!-- comment -->\n- Item";
2511 let fixed = fix(content);
2512 assert_eq!(fixed, content, "Fix should not modify already-correct content");
2513 }
2514
2515 #[test]
2516 fn test_html_comment_fix_adds_blank_when_needed() {
2517 let content = "Text.\n<!-- comment -->\n- Item";
2520 let fixed = fix(content);
2521 assert!(
2522 fixed.contains("<!-- comment -->\n\n- Item"),
2523 "Fix should add blank line before list. Got: {fixed}"
2524 );
2525 }
2526
2527 #[test]
2528 fn test_ordered_list_inside_html_comment() {
2529 let content = "<!--\n3. Starting at 3\n4. Next item\n-->";
2531 let warnings = lint(content);
2532 assert_eq!(
2533 warnings.len(),
2534 0,
2535 "Should not warn about ordered list inside HTML comment. Got: {warnings:?}"
2536 );
2537 }
2538
2539 #[test]
2546 fn test_blockquote_list_exit_no_warning() {
2547 let content = "- outer item\n > - blockquote list 1\n > - blockquote list 2\n- next outer item";
2549 let warnings = lint(content);
2550 assert_eq!(
2551 warnings.len(),
2552 0,
2553 "Should not warn when exiting blockquote. Got: {warnings:?}"
2554 );
2555 }
2556
2557 #[test]
2558 fn test_nested_blockquote_list_exit() {
2559 let content = "- outer\n - nested\n > - bq list 1\n > - bq list 2\n - back to nested\n- outer again";
2561 let warnings = lint(content);
2562 assert_eq!(
2563 warnings.len(),
2564 0,
2565 "Should not warn when exiting nested blockquote list. Got: {warnings:?}"
2566 );
2567 }
2568
2569 #[test]
2570 fn test_blockquote_same_level_no_warning() {
2571 let content = "> - item 1\n> - item 2\n> Text after";
2574 let warnings = lint(content);
2575 assert_eq!(
2576 warnings.len(),
2577 0,
2578 "Should not warn - text is lazy continuation in blockquote. Got: {warnings:?}"
2579 );
2580 }
2581
2582 #[test]
2583 fn test_blockquote_list_with_special_chars() {
2584 let content = "- Item with <>&\n > - blockquote item\n- Back to outer";
2586 let warnings = lint(content);
2587 assert_eq!(
2588 warnings.len(),
2589 0,
2590 "Special chars in content should not affect blockquote detection. Got: {warnings:?}"
2591 );
2592 }
2593
2594 #[test]
2595 fn test_lazy_continuation_whitespace_only_line() {
2596 let content = "- Item\n \nText after whitespace-only line";
2599 let config = MD032Config {
2600 allow_lazy_continuation: false,
2601 };
2602 let warnings = lint_with_config(content, config);
2603 assert_eq!(
2605 warnings.len(),
2606 0,
2607 "Whitespace-only line IS a separator in CommonMark. Got: {warnings:?}"
2608 );
2609 }
2610
2611 #[test]
2612 fn test_lazy_continuation_blockquote_context() {
2613 let content = "> - Item\n> Lazy in quote";
2615 let config = MD032Config {
2616 allow_lazy_continuation: false,
2617 };
2618 let warnings = lint_with_config(content, config);
2619 assert!(warnings.len() <= 1, "Should handle blockquote context gracefully");
2622 }
2623
2624 #[test]
2625 fn test_lazy_continuation_fix_preserves_content() {
2626 let content = "- Item with special chars: <>&\nContinuation with: \"quotes\"";
2628 let config = MD032Config {
2629 allow_lazy_continuation: false,
2630 };
2631 let fixed = fix_with_config(content, config);
2632 assert!(fixed.contains("<>&"), "Should preserve special chars");
2633 assert!(fixed.contains("\"quotes\""), "Should preserve quotes");
2634 assert_eq!(fixed, "- Item with special chars: <>&\n Continuation with: \"quotes\"");
2636 }
2637
2638 #[test]
2639 fn test_lazy_continuation_fix_idempotent() {
2640 let content = "- Item\nLazy";
2642 let config = MD032Config {
2643 allow_lazy_continuation: false,
2644 };
2645 let fixed_once = fix_with_config(content, config.clone());
2646 let fixed_twice = fix_with_config(&fixed_once, config);
2647 assert_eq!(fixed_once, fixed_twice, "Fix should be idempotent");
2648 }
2649
2650 #[test]
2651 fn test_lazy_continuation_config_default_allows() {
2652 let content = "- Item\nLazy text that continues";
2654 let default_config = MD032Config::default();
2655 assert!(
2656 default_config.allow_lazy_continuation,
2657 "Default should allow lazy continuation"
2658 );
2659 let warnings = lint_with_config(content, default_config);
2660 assert_eq!(warnings.len(), 0, "Default config should not warn on lazy continuation");
2661 }
2662
2663 #[test]
2664 fn test_lazy_continuation_after_multi_line_item() {
2665 let content = "- Item line 1\n Item line 2 (indented)\nLazy (not indented)";
2667 let config = MD032Config {
2668 allow_lazy_continuation: false,
2669 };
2670 let warnings = lint_with_config(content, config.clone());
2671 assert_eq!(
2672 warnings.len(),
2673 1,
2674 "Should warn only for the lazy line, not the indented line"
2675 );
2676 }
2677
2678 #[test]
2680 fn test_blockquote_list_with_continuation_and_nested() {
2681 let content = "> - item 1\n> continuation\n> - nested\n> - item 2";
2684 let warnings = lint(content);
2685 assert_eq!(
2686 warnings.len(),
2687 0,
2688 "Blockquoted list with continuation and nested items should have no warnings. Got: {warnings:?}"
2689 );
2690 }
2691
2692 #[test]
2693 fn test_blockquote_list_simple() {
2694 let content = "> - item 1\n> - item 2";
2696 let warnings = lint(content);
2697 assert_eq!(warnings.len(), 0, "Simple blockquoted list should have no warnings");
2698 }
2699
2700 #[test]
2701 fn test_blockquote_list_with_continuation_only() {
2702 let content = "> - item 1\n> continuation\n> - item 2";
2704 let warnings = lint(content);
2705 assert_eq!(
2706 warnings.len(),
2707 0,
2708 "Blockquoted list with continuation should have no warnings"
2709 );
2710 }
2711
2712 #[test]
2713 fn test_blockquote_list_with_lazy_continuation() {
2714 let content = "> - item 1\n> lazy continuation\n> - item 2";
2716 let warnings = lint(content);
2717 assert_eq!(
2718 warnings.len(),
2719 0,
2720 "Blockquoted list with lazy continuation should have no warnings"
2721 );
2722 }
2723
2724 #[test]
2725 fn test_nested_blockquote_list() {
2726 let content = ">> - item 1\n>> continuation\n>> - nested\n>> - item 2";
2728 let warnings = lint(content);
2729 assert_eq!(warnings.len(), 0, "Nested blockquote list should have no warnings");
2730 }
2731
2732 #[test]
2733 fn test_blockquote_list_needs_preceding_blank() {
2734 let content = "> Text before\n> - item 1\n> - item 2";
2736 let warnings = lint(content);
2737 assert_eq!(
2738 warnings.len(),
2739 1,
2740 "Should warn for missing blank before blockquoted list"
2741 );
2742 }
2743
2744 #[test]
2745 fn test_blockquote_list_properly_separated() {
2746 let content = "> Text before\n>\n> - item 1\n> - item 2\n>\n> Text after";
2748 let warnings = lint(content);
2749 assert_eq!(
2750 warnings.len(),
2751 0,
2752 "Properly separated blockquoted list should have no warnings"
2753 );
2754 }
2755
2756 #[test]
2757 fn test_blockquote_ordered_list() {
2758 let content = "> 1. item 1\n> continuation\n> 2. item 2";
2760 let warnings = lint(content);
2761 assert_eq!(warnings.len(), 0, "Ordered list in blockquote should have no warnings");
2762 }
2763
2764 #[test]
2765 fn test_blockquote_list_with_empty_blockquote_line() {
2766 let content = "> - item 1\n>\n> - item 2";
2768 let warnings = lint(content);
2769 assert_eq!(warnings.len(), 0, "Empty blockquote line should not break list");
2770 }
2771
2772 #[test]
2774 fn test_blockquote_list_multi_paragraph_items() {
2775 let content = "# Test\n\n> Some intro text\n> \n> * List item 1\n> \n> Continuation\n> * List item 2\n";
2778 let warnings = lint(content);
2779 assert_eq!(
2780 warnings.len(),
2781 0,
2782 "Multi-paragraph list items in blockquotes should have no warnings. Got: {warnings:?}"
2783 );
2784 }
2785
2786 #[test]
2788 fn test_blockquote_ordered_list_multi_paragraph_items() {
2789 let content = "> 1. First item\n> \n> Continuation of first\n> 2. Second item\n";
2790 let warnings = lint(content);
2791 assert_eq!(
2792 warnings.len(),
2793 0,
2794 "Ordered multi-paragraph list items in blockquotes should have no warnings. Got: {warnings:?}"
2795 );
2796 }
2797
2798 #[test]
2800 fn test_blockquote_list_multiple_continuations() {
2801 let content = "> - Item 1\n> \n> First continuation\n> \n> Second continuation\n> - Item 2\n";
2802 let warnings = lint(content);
2803 assert_eq!(
2804 warnings.len(),
2805 0,
2806 "Multiple continuation paragraphs should not break blockquote list. Got: {warnings:?}"
2807 );
2808 }
2809
2810 #[test]
2812 fn test_nested_blockquote_multi_paragraph_list() {
2813 let content = ">> - Item 1\n>> \n>> Continuation\n>> - Item 2\n";
2814 let warnings = lint(content);
2815 assert_eq!(
2816 warnings.len(),
2817 0,
2818 "Nested blockquote multi-paragraph list should have no warnings. Got: {warnings:?}"
2819 );
2820 }
2821
2822 #[test]
2824 fn test_triple_nested_blockquote_multi_paragraph_list() {
2825 let content = ">>> - Item 1\n>>> \n>>> Continuation\n>>> - Item 2\n";
2826 let warnings = lint(content);
2827 assert_eq!(
2828 warnings.len(),
2829 0,
2830 "Triple-nested blockquote multi-paragraph list should have no warnings. Got: {warnings:?}"
2831 );
2832 }
2833
2834 #[test]
2836 fn test_blockquote_list_last_item_continuation() {
2837 let content = "> - Item 1\n> - Item 2\n> \n> Continuation of item 2\n";
2838 let warnings = lint(content);
2839 assert_eq!(
2840 warnings.len(),
2841 0,
2842 "Last item with continuation should have no warnings. Got: {warnings:?}"
2843 );
2844 }
2845
2846 #[test]
2848 fn test_blockquote_list_first_item_only_continuation() {
2849 let content = "> - Item 1\n> \n> Continuation of item 1\n";
2850 let warnings = lint(content);
2851 assert_eq!(
2852 warnings.len(),
2853 0,
2854 "Single item with continuation should have no warnings. Got: {warnings:?}"
2855 );
2856 }
2857
2858 #[test]
2862 fn test_blockquote_level_change_breaks_list() {
2863 let content = "> - Item in single blockquote\n>> - Item in nested blockquote\n";
2865 let warnings = lint(content);
2866 assert!(
2870 warnings.len() <= 2,
2871 "Blockquote level change warnings should be reasonable. Got: {warnings:?}"
2872 );
2873 }
2874
2875 #[test]
2877 fn test_exit_blockquote_needs_blank_before_list() {
2878 let content = "> Blockquote text\n\n- List outside blockquote\n";
2880 let warnings = lint(content);
2881 assert_eq!(
2882 warnings.len(),
2883 0,
2884 "List after blank line outside blockquote should be fine. Got: {warnings:?}"
2885 );
2886
2887 let content2 = "> Blockquote text\n- List outside blockquote\n";
2891 let warnings2 = lint(content2);
2892 assert!(
2894 warnings2.len() <= 1,
2895 "List after blockquote warnings should be reasonable. Got: {warnings2:?}"
2896 );
2897 }
2898
2899 #[test]
2901 fn test_blockquote_multi_paragraph_all_unordered_markers() {
2902 let content_dash = "> - Item 1\n> \n> Continuation\n> - Item 2\n";
2904 let warnings = lint(content_dash);
2905 assert_eq!(warnings.len(), 0, "Dash marker should work. Got: {warnings:?}");
2906
2907 let content_asterisk = "> * Item 1\n> \n> Continuation\n> * Item 2\n";
2909 let warnings = lint(content_asterisk);
2910 assert_eq!(warnings.len(), 0, "Asterisk marker should work. Got: {warnings:?}");
2911
2912 let content_plus = "> + Item 1\n> \n> Continuation\n> + Item 2\n";
2914 let warnings = lint(content_plus);
2915 assert_eq!(warnings.len(), 0, "Plus marker should work. Got: {warnings:?}");
2916 }
2917
2918 #[test]
2920 fn test_blockquote_multi_paragraph_parenthesis_marker() {
2921 let content = "> 1) Item 1\n> \n> Continuation\n> 2) Item 2\n";
2922 let warnings = lint(content);
2923 assert_eq!(
2924 warnings.len(),
2925 0,
2926 "Parenthesis ordered markers should work. Got: {warnings:?}"
2927 );
2928 }
2929
2930 #[test]
2932 fn test_blockquote_multi_paragraph_multi_digit_numbers() {
2933 let content = "> 10. Item 10\n> \n> Continuation of item 10\n> 11. Item 11\n";
2935 let warnings = lint(content);
2936 assert_eq!(
2937 warnings.len(),
2938 0,
2939 "Multi-digit ordered list should work. Got: {warnings:?}"
2940 );
2941 }
2942
2943 #[test]
2945 fn test_blockquote_multi_paragraph_with_formatting() {
2946 let content = "> - Item with **bold**\n> \n> Continuation with *emphasis* and `code`\n> - Item 2\n";
2947 let warnings = lint(content);
2948 assert_eq!(
2949 warnings.len(),
2950 0,
2951 "Continuation with inline formatting should work. Got: {warnings:?}"
2952 );
2953 }
2954
2955 #[test]
2957 fn test_blockquote_multi_paragraph_all_items_have_continuation() {
2958 let content = "> - Item 1\n> \n> Continuation 1\n> - Item 2\n> \n> Continuation 2\n> - Item 3\n> \n> Continuation 3\n";
2959 let warnings = lint(content);
2960 assert_eq!(
2961 warnings.len(),
2962 0,
2963 "All items with continuations should work. Got: {warnings:?}"
2964 );
2965 }
2966
2967 #[test]
2969 fn test_blockquote_multi_paragraph_lowercase_continuation() {
2970 let content = "> - Item 1\n> \n> and this continues the item\n> - Item 2\n";
2971 let warnings = lint(content);
2972 assert_eq!(
2973 warnings.len(),
2974 0,
2975 "Lowercase continuation should work. Got: {warnings:?}"
2976 );
2977 }
2978
2979 #[test]
2981 fn test_blockquote_multi_paragraph_uppercase_continuation() {
2982 let content = "> - Item 1\n> \n> This continues the item with uppercase\n> - Item 2\n";
2983 let warnings = lint(content);
2984 assert_eq!(
2985 warnings.len(),
2986 0,
2987 "Uppercase continuation with proper indent should work. Got: {warnings:?}"
2988 );
2989 }
2990
2991 #[test]
2993 fn test_blockquote_separate_ordered_unordered_multi_paragraph() {
2994 let content = "> - Unordered item\n> \n> Continuation\n> \n> 1. Ordered item\n> \n> Continuation\n";
2996 let warnings = lint(content);
2997 assert!(
2999 warnings.len() <= 1,
3000 "Separate lists with continuations should be reasonable. Got: {warnings:?}"
3001 );
3002 }
3003
3004 #[test]
3006 fn test_blockquote_multi_paragraph_bare_marker_blank() {
3007 let content = "> - Item 1\n>\n> Continuation\n> - Item 2\n";
3009 let warnings = lint(content);
3010 assert_eq!(warnings.len(), 0, "Bare > as blank line should work. Got: {warnings:?}");
3011 }
3012
3013 #[test]
3014 fn test_blockquote_list_varying_spaces_after_marker() {
3015 let content = "> - item 1\n> continuation with more indent\n> - item 2";
3017 let warnings = lint(content);
3018 assert_eq!(warnings.len(), 0, "Varying spaces after > should not break list");
3019 }
3020
3021 #[test]
3022 fn test_deeply_nested_blockquote_list() {
3023 let content = ">>> - item 1\n>>> continuation\n>>> - item 2";
3025 let warnings = lint(content);
3026 assert_eq!(
3027 warnings.len(),
3028 0,
3029 "Deeply nested blockquote list should have no warnings"
3030 );
3031 }
3032
3033 #[test]
3034 fn test_blockquote_level_change_in_list() {
3035 let content = "> - item 1\n>> - deeper item\n> - item 2";
3037 let warnings = lint(content);
3040 assert!(
3041 !warnings.is_empty(),
3042 "Blockquote level change should break list and trigger warnings"
3043 );
3044 }
3045
3046 #[test]
3047 fn test_blockquote_list_with_code_span() {
3048 let content = "> - item with `code`\n> continuation\n> - item 2";
3050 let warnings = lint(content);
3051 assert_eq!(
3052 warnings.len(),
3053 0,
3054 "Blockquote list with code span should have no warnings"
3055 );
3056 }
3057
3058 #[test]
3059 fn test_code_span_html_comment_delimiters_no_false_positive() {
3060 let content = "Text before list.\n\n1. A list item with `<!--` in a code span\n\n### Heading After\n\n1. Another item with `-->` in it\n";
3066 let warnings = lint(content);
3067 assert_eq!(
3068 warnings.len(),
3069 0,
3070 "code-span HTML comment delimiters must not cause MD032 false positives, got: {warnings:?}"
3071 );
3072 }
3073
3074 #[test]
3075 fn test_code_span_html_comment_delimiters_fix_is_idempotent() {
3076 let content = "Text before list.\n\n1. A list item with `<!--` in a code span\n\n### Heading After\n\n1. Another item with `-->` in it\n";
3081 let fixed = fix(content);
3082 assert_eq!(
3083 fixed, content,
3084 "MD032 fix must be a no-op for content whose only `<!--`/`-->` are inside code spans"
3085 );
3086 }
3087
3088 #[test]
3089 fn test_blockquote_list_at_document_end() {
3090 let content = "> Some text\n>\n> - item 1\n> - item 2";
3092 let warnings = lint(content);
3093 assert_eq!(
3094 warnings.len(),
3095 0,
3096 "Blockquote list at document end should have no warnings"
3097 );
3098 }
3099
3100 #[test]
3101 fn test_fix_preserves_blockquote_prefix_before_list() {
3102 let content = "> Text before
3104> - Item 1
3105> - Item 2";
3106 let fixed = fix(content);
3107
3108 let expected = "> Text before
3110>
3111> - Item 1
3112> - Item 2";
3113 assert_eq!(
3114 fixed, expected,
3115 "Fix should insert '>' blank line, not plain blank line"
3116 );
3117 }
3118
3119 #[test]
3120 fn test_fix_preserves_triple_nested_blockquote_prefix_for_list() {
3121 let content = ">>> Triple nested
3124>>> - Item 1
3125>>> - Item 2
3126>>> More text";
3127 let fixed = fix(content);
3128
3129 let expected = ">>> Triple nested
3131>>>
3132>>> - Item 1
3133>>> - Item 2
3134>>> More text";
3135 assert_eq!(
3136 fixed, expected,
3137 "Fix should preserve triple-nested blockquote prefix '>>>'"
3138 );
3139 }
3140
3141 fn lint_quarto(content: &str) -> Vec<LintWarning> {
3144 let rule = MD032BlanksAroundLists::default();
3145 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
3146 rule.check(&ctx).unwrap()
3147 }
3148
3149 #[test]
3150 fn test_quarto_list_after_div_open() {
3151 let content = "Content\n\n::: {.callout-note}\n- Item 1\n- Item 2\n:::\n";
3153 let warnings = lint_quarto(content);
3154 assert!(
3156 warnings.is_empty(),
3157 "Quarto div marker should be transparent before list: {warnings:?}"
3158 );
3159 }
3160
3161 #[test]
3162 fn test_quarto_list_before_div_close() {
3163 let content = "::: {.callout-note}\n\n- Item 1\n- Item 2\n:::\n";
3165 let warnings = lint_quarto(content);
3166 assert!(
3168 warnings.is_empty(),
3169 "Quarto div marker should be transparent after list: {warnings:?}"
3170 );
3171 }
3172
3173 #[test]
3174 fn test_quarto_list_needs_blank_without_div() {
3175 let content = "Content\n::: {.callout-note}\n- Item 1\n- Item 2\n:::\n";
3177 let warnings = lint_quarto(content);
3178 assert!(
3181 !warnings.is_empty(),
3182 "Should still require blank when not present: {warnings:?}"
3183 );
3184 }
3185
3186 #[test]
3187 fn test_quarto_list_in_callout_with_content() {
3188 let content = "::: {.callout-note}\nNote introduction:\n\n- Item 1\n- Item 2\n\nMore note content.\n:::\n";
3190 let warnings = lint_quarto(content);
3191 assert!(
3192 warnings.is_empty(),
3193 "List with proper blanks inside callout should pass: {warnings:?}"
3194 );
3195 }
3196
3197 #[test]
3198 fn test_quarto_div_markers_not_transparent_in_standard_flavor() {
3199 let content = "Content\n\n:::\n- Item 1\n- Item 2\n:::\n";
3201 let warnings = lint(content); assert!(
3204 !warnings.is_empty(),
3205 "Standard flavor should not treat ::: as transparent: {warnings:?}"
3206 );
3207 }
3208
3209 #[test]
3210 fn test_quarto_nested_divs_with_list() {
3211 let content = "::: {.outer}\n::: {.inner}\n\n- Item 1\n- Item 2\n\n:::\n:::\n";
3213 let warnings = lint_quarto(content);
3214 assert!(warnings.is_empty(), "Nested divs with list should work: {warnings:?}");
3215 }
3216
3217 #[test]
3218 fn test_issue512_complex_nested_list_with_continuation() {
3219 let content = "\
3222- First level of indentation.
3223 - Second level of indentation.
3224 - Third level of indentation.
3225 - Third level of indentation.
3226
3227 Second level list continuation.
3228
3229 First level list continuation.
3230- First level of indentation.
3231";
3232 let warnings = lint(content);
3233 assert!(
3234 warnings.is_empty(),
3235 "Nested list with parent-level continuation should produce no warnings. Got: {warnings:?}"
3236 );
3237 }
3238
3239 #[test]
3240 fn test_issue512_continuation_at_root_level() {
3241 let content = "\
3245- First level.
3246 - Second level.
3247
3248 First level continuation.
3249
3250Root level lazy continuation.
3251- Another first level item.
3252";
3253 let warnings = lint(content);
3254 assert_eq!(
3255 warnings.len(),
3256 1,
3257 "Should warn on line 7 (new list after break). Got: {warnings:?}"
3258 );
3259 assert_eq!(warnings[0].line, 7);
3260 }
3261
3262 #[test]
3263 fn test_issue512_three_level_nesting_continuation_at_each_level() {
3264 let content = "\
3266- Level 1 item.
3267 - Level 2 item.
3268 - Level 3 item.
3269
3270 Level 3 continuation.
3271
3272 Level 2 continuation.
3273
3274 Level 1 continuation (indented under marker).
3275- Another level 1 item.
3276";
3277 let warnings = lint(content);
3278 assert!(
3279 warnings.is_empty(),
3280 "Continuation at each nesting level should produce no warnings. Got: {warnings:?}"
3281 );
3282 }
3283
3284 #[test]
3285 fn test_pandoc_list_after_div_open() {
3286 let rule = MD032BlanksAroundLists::default();
3289 let content = "Content\n\n::: {.callout-note}\n- Item 1\n- Item 2\n:::\n";
3290 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
3291 let warnings = rule.check(&ctx).unwrap();
3292 assert!(
3293 warnings.is_empty(),
3294 "MD032 should treat Pandoc div marker as transparent before list: {warnings:?}"
3295 );
3296 }
3297
3298 #[test]
3299 fn test_md032_html_comment() {
3300 let rule = MD032BlanksAroundLists::default();
3301 let content = "text\n<!--\n- Item 1\n- Item 2\n-->\ntext";
3302 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3303 let warnings = rule.check(&ctx).unwrap();
3304 assert!(
3305 warnings.is_empty(),
3306 "MD032 should not require blank lines around lists inside HTML comments: {warnings:?}"
3307 );
3308 }
3309
3310 #[test]
3311 fn test_mkdocs_admonition_nested_ordered_list_not_flagged() {
3312 let rule = MD032BlanksAroundLists::default();
3318 let content = "1. no error here\n\n!!! example\n\n 1. no error here.\n 2. error here because previous line ends with a \".\"\n 3. no error here\n";
3319 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3320 let warnings = rule.check(&ctx).unwrap();
3321 assert!(
3322 warnings.is_empty(),
3323 "admonition-nested ordered list should not be flagged: {warnings:?}"
3324 );
3325 }
3326
3327 #[test]
3328 fn test_mkdocs_admonition_nested_ordered_list_cascade_not_flagged() {
3329 let rule = MD032BlanksAroundLists::default();
3333 let content = "1. no error here\n\n!!! example\n\n 1. no error here.\n 2. error here because previous line ends with a period.\n 3. no error here\n";
3334 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3335 let warnings = rule.check(&ctx).unwrap();
3336 assert!(
3337 warnings.is_empty(),
3338 "cascading admonition-nested ordered list should not be flagged: {warnings:?}"
3339 );
3340 }
3341
3342 #[test]
3343 fn test_mkdocs_content_tab_nested_ordered_list_not_flagged() {
3344 let rule = MD032BlanksAroundLists::default();
3346 let content = "1. no error here\n\n=== \"Tab A\"\n\n 1. no error here.\n 2. error here because previous line ends with a \".\"\n 3. no error here\n";
3347 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3348 let warnings = rule.check(&ctx).unwrap();
3349 assert!(
3350 warnings.is_empty(),
3351 "content-tab-nested ordered list should not be flagged: {warnings:?}"
3352 );
3353 }
3354
3355 #[test]
3356 fn test_mkdocs_admonition_prose_then_non1_item_still_flagged() {
3357 let rule = MD032BlanksAroundLists::default();
3363 let content = "1. no error here\n\n!!! example\n\n Intro.\n 2. item\n";
3364 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3365 let warnings = rule.check(&ctx).unwrap();
3366 assert_eq!(
3367 warnings.len(),
3368 1,
3369 "prose then non-1 item inside an admonition must stay flagged: {warnings:?}"
3370 );
3371 }
3372
3373 #[test]
3374 fn test_mkdocs_admonition_prose_after_list_item_breaks_continuation() {
3375 let rule = MD032BlanksAroundLists::default();
3380 let content = "1. no error here\n\n!!! example\n\n 1. one.\n Intro prose.\n 2. two\n";
3381 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3382 let warnings = rule.check(&ctx).unwrap();
3383 assert_eq!(
3384 warnings.len(),
3385 1,
3386 "prose at item indent breaks the list continuation, item must stay flagged: {warnings:?}"
3387 );
3388 }
3389
3390 #[test]
3391 fn test_mkdocs_admonition_wrapped_item_continuation_not_flagged() {
3392 let rule = MD032BlanksAroundLists::default();
3397 let content = "1. no error here\n\n!!! example\n\n 1. item one that wraps\n onto a second line.\n 2. item two\n";
3398 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3399 let warnings = rule.check(&ctx).unwrap();
3400 assert!(
3401 warnings.is_empty(),
3402 "wrapped continuation of a nested list item must not be flagged: {warnings:?}"
3403 );
3404 }
3405
3406 #[test]
3407 fn test_mkdocs_ambiguous_prose_non1_ordered_item_still_flagged() {
3408 let rule = MD032BlanksAroundLists::default();
3414 let content = "1. no error here\n\nno error here.\n2. error here because previous line ends with a period.\n";
3415 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3416 let warnings = rule.check(&ctx).unwrap();
3417 assert_eq!(
3418 warnings.len(),
3419 1,
3420 "ambiguous non-1 ordered item outside any container should still be flagged: {warnings:?}"
3421 );
3422 assert_eq!(warnings[0].line, 4);
3423 assert!(warnings[0].message.contains("non-1"));
3424 }
3425
3426 #[test]
3427 fn test_mkdocs_admonition_nested_list_without_trailing_punctuation_not_flagged() {
3428 let rule = MD032BlanksAroundLists::default();
3434 let content = "1. no error here\n\n!!! example\n\n 1. no error here\n 2. error here because previous line ends with a \".\"\n 3. no error here\n";
3435 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3436 let warnings = rule.check(&ctx).unwrap();
3437 assert!(
3438 warnings.is_empty(),
3439 "admonition-nested ordered list without trailing punctuation should not be flagged: {warnings:?}"
3440 );
3441 }
3442
3443 #[test]
3444 fn test_standard_flavor_admonition_indented_list_unchanged() {
3445 let rule = MD032BlanksAroundLists::default();
3450 let content = "1. no error here\n\n!!! example\n\n 1. no error here.\n 2. error here because previous line ends with a \".\"\n 3. no error here\n";
3451 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3452 let warnings = rule.check(&ctx).unwrap();
3453 assert!(
3454 warnings.is_empty(),
3455 "indented code block under standard flavor should not be flagged: {warnings:?}"
3456 );
3457 }
3458
3459 #[test]
3460 fn test_mkdocs_html_markdown_div_nested_ordered_list_still_flagged() {
3461 let rule = MD032BlanksAroundLists::default();
3466 let content = "1. no error here\n\n<div markdown=\"1\">\n\n 1. no error here.\n 2. error here because previous line ends with a \".\"\n 3. no error here\n\n</div>\n";
3467 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3468 let warnings = rule.check(&ctx).unwrap();
3469 assert_eq!(
3470 warnings.len(),
3471 1,
3472 "markdown=\"1\" div nested ordered list behavior must stay unchanged: {warnings:?}"
3473 );
3474 assert_eq!(warnings[0].line, 6);
3475 }
3476}