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::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 ) -> Vec<LintWarning> {
502 let mut warnings = Vec::new();
503 let num_lines = lines.len();
504
505 for (line_idx, line) in lines.iter().enumerate() {
508 let line_num = line_idx + 1;
509
510 let is_in_list = list_blocks
512 .iter()
513 .any(|(start, end, _)| line_num >= *start && line_num <= *end);
514 if is_in_list {
515 continue;
516 }
517
518 if ctx.line_info(line_num).is_some_and(|info| {
520 info.in_code_block
521 || info.in_front_matter
522 || info.in_html_comment
523 || info.in_mdx_comment
524 || info.in_html_block
525 || info.in_jsx_block
526 }) {
527 continue;
528 }
529
530 if ORDERED_LIST_NON_ONE_RE.is_match(line) {
532 if line_idx > 0 {
534 let prev_line = lines[line_idx - 1];
535 let prev_is_blank = is_blank_in_context(prev_line);
536 let prev_line_info = ctx.line_info(line_idx);
537 let prev_excluded = prev_line_info.is_some_and(|info| info.in_code_block || info.in_front_matter);
538
539 let prev_in_mkdocs_container =
555 prev_line_info.is_some_and(|info| info.in_admonition || info.in_content_tab);
556 let continues_stale_container_list = prev_in_mkdocs_container && {
557 let item_indent = calculate_indentation_width_default(line);
558 let mut found_marker = false;
559 for j in (0..line_idx).rev() {
560 let in_container = ctx
561 .line_info(j + 1)
562 .is_some_and(|info| info.in_admonition || info.in_content_tab);
563 if !in_container {
564 break;
565 }
566 let candidate = lines[j];
567 if is_blank_in_context(candidate) {
568 continue;
569 }
570 let candidate_indent = calculate_indentation_width_default(candidate);
571 if crate::utils::regex_cache::ORDERED_LIST_MARKER_REGEX.is_match(candidate)
572 && candidate_indent == item_indent
573 {
574 found_marker = true;
575 break;
576 }
577 if candidate_indent <= item_indent {
578 break;
579 }
580 }
581 found_marker
582 };
583
584 let prev_trimmed = prev_line.trim();
589 let is_sentence_continuation = continues_stale_container_list
590 || (!prev_is_blank
591 && !prev_trimmed.is_empty()
592 && !prev_trimmed.ends_with('.')
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
601 if prev_is_blank || !is_sentence_continuation {
602 if !prev_is_blank && !prev_excluded {
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"
615 .to_string(),
616 fix: Some(Fix::new(
617 ctx.line_column_byte_range_with_length(line_num, 1, 0),
618 format!("{bq_prefix}\n"),
619 )),
620 });
621 }
622
623 if line_idx + 1 < num_lines {
626 let next_line = lines[line_idx + 1];
627 let next_is_blank = is_blank_in_context(next_line);
628 let next_excluded = ctx.line_info(line_idx + 2).is_some_and(|info| info.in_front_matter);
629
630 if !next_is_blank && !next_excluded && !next_line.trim().is_empty() {
631 let next_trimmed = next_line.trim_start();
635 let next_is_ordered_content = ORDERED_LIST_NON_ONE_RE.is_match(next_line)
636 || next_line.starts_with("1. ")
637 || (next_line.len() > next_trimmed.len()
638 && !next_trimmed.starts_with("- ")
639 && !next_trimmed.starts_with("* ")
640 && !next_trimmed.starts_with("+ ")); if !next_is_ordered_content {
643 let (start_line, start_col, end_line, end_col) =
644 calculate_line_range(line_num, line);
645 let bq_prefix = ctx.blockquote_prefix_for_blank_line(line_idx);
646 warnings.push(LintWarning {
647 line: start_line,
648 column: start_col,
649 end_line,
650 end_column: end_col,
651 severity: Severity::Warning,
652 rule_name: Some(self.name().to_string()),
653 message: "List should be followed by blank line".to_string(),
654 fix: Some(Fix::new(
655 ctx.line_column_byte_range_with_length(line_num + 1, 1, 0),
656 format!("{bq_prefix}\n"),
657 )),
658 });
659 }
660 }
661 }
662 }
663 }
664 }
665 }
666
667 for &(start_line, end_line, ref prefix) in list_blocks {
668 if ctx
670 .line_info(start_line)
671 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
672 {
673 continue;
674 }
675
676 if start_line > 1 {
677 let (content_line, has_blank_separation) = Self::find_preceding_content(ctx, start_line);
679
680 if !has_blank_separation && content_line > 0 {
682 let prev_line_str = lines[content_line - 1];
683 let is_prev_excluded = ctx
684 .line_info(content_line)
685 .is_some_and(|info| info.in_code_block || info.in_front_matter);
686 let prev_prefix = BLOCKQUOTE_PREFIX_RE.find(prev_line_str).map_or("", |m| m.as_str());
687 let prefixes_match = prev_prefix.trim() == prefix.trim();
688
689 let should_require = Self::should_require_blank_line_before(ctx, content_line, start_line);
692 if !is_prev_excluded && prefixes_match && should_require {
693 let (start_line, start_col, end_line, end_col) =
695 calculate_line_range(start_line, lines[start_line - 1]);
696
697 warnings.push(LintWarning {
698 line: start_line,
699 column: start_col,
700 end_line,
701 end_column: end_col,
702 severity: Severity::Warning,
703 rule_name: Some(self.name().to_string()),
704 message: "List should be preceded by blank line".to_string(),
705 fix: Some(Fix::new(
706 ctx.line_column_byte_range_with_length(start_line, 1, 0),
707 format!("{prefix}\n"),
708 )),
709 });
710 }
711 }
712 }
713
714 if end_line < num_lines {
715 let (content_line, has_blank_separation) = Self::find_following_content(ctx, end_line);
717
718 if !has_blank_separation && content_line > 0 {
720 let next_line_str = lines[content_line - 1];
721 let is_next_excluded = ctx.line_info(content_line).is_some_and(|info| info.in_front_matter)
724 || (content_line <= ctx.lines.len()
725 && ctx.lines[content_line - 1].in_code_block
726 && ctx.lines[content_line - 1].indent >= 2);
727 let next_prefix = BLOCKQUOTE_PREFIX_RE.find(next_line_str).map_or("", |m| m.as_str());
728
729 let end_line_str = lines[end_line - 1];
734 let end_line_prefix = BLOCKQUOTE_PREFIX_RE.find(end_line_str).map_or("", |m| m.as_str());
735 let end_line_bq_level = end_line_prefix.chars().filter(|&c| c == '>').count();
736 let next_line_bq_level = next_prefix.chars().filter(|&c| c == '>').count();
737 let exits_blockquote = end_line_bq_level > 0 && next_line_bq_level < end_line_bq_level;
738
739 let prefixes_match = next_prefix.trim() == prefix.trim();
740
741 let is_tight_continuation_of_last_item = ctx
747 .lines
748 .get(end_line - 1)
749 .and_then(|last_li| last_li.list_item.as_ref())
750 .is_some_and(|last_item| {
751 let marker_col = last_item.marker_column;
752 ctx.lines.get(content_line - 1).is_some_and(|next_li| {
753 !next_li.is_blank && next_li.list_item.is_none() && next_li.indent > marker_col
754 })
755 });
756
757 if !is_next_excluded && prefixes_match && !exits_blockquote && !is_tight_continuation_of_last_item {
760 let (start_line_last, start_col_last, end_line_last, end_col_last) =
762 calculate_line_range(end_line, lines[end_line - 1]);
763
764 warnings.push(LintWarning {
765 line: start_line_last,
766 column: start_col_last,
767 end_line: end_line_last,
768 end_column: end_col_last,
769 severity: Severity::Warning,
770 rule_name: Some(self.name().to_string()),
771 message: "List should be followed by blank line".to_string(),
772 fix: Some(Fix::new(
773 ctx.line_column_byte_range_with_length(end_line + 1, 1, 0),
774 format!("{prefix}\n"),
775 )),
776 });
777 }
778 }
779 }
780 }
781 warnings
782 }
783}
784
785impl Rule for MD032BlanksAroundLists {
786 fn name(&self) -> &'static str {
787 "MD032"
788 }
789
790 fn description(&self) -> &'static str {
791 "Lists should be surrounded by blank lines"
792 }
793
794 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
795 let lines = ctx.raw_lines();
796 if lines.is_empty() {
798 return Ok(Vec::new());
799 }
800
801 let list_blocks = self.convert_list_blocks(ctx);
802
803 if list_blocks.is_empty() {
804 return Ok(Vec::new());
805 }
806
807 let mut warnings = self.perform_checks(ctx, lines, &list_blocks);
808
809 if !self.config.allow_lazy_continuation {
814 let lazy_cont_lines = ctx.lazy_continuation_lines();
815
816 for lazy_info in lazy_cont_lines.iter() {
817 let line_num = lazy_info.line_num;
818
819 let is_within_block = list_blocks
823 .iter()
824 .any(|(start, end, _)| line_num >= *start && line_num <= *end);
825
826 if !is_within_block {
827 continue;
828 }
829
830 let line_content = lines.get(line_num.saturating_sub(1)).unwrap_or(&"");
832 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_num, line_content);
833
834 let fix = if Self::should_apply_lazy_fix(ctx, line_num) {
836 Self::calculate_lazy_continuation_fix(ctx, line_num, lazy_info)
837 } else {
838 None
839 };
840
841 warnings.push(LintWarning {
842 line: start_line,
843 column: start_col,
844 end_line,
845 end_column: end_col,
846 severity: Severity::Warning,
847 rule_name: Some(self.name().to_string()),
848 message: "Lazy continuation line should be properly indented or preceded by blank line".to_string(),
849 fix,
850 });
851 }
852 }
853
854 Ok(warnings)
855 }
856
857 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
858 Ok(self.fix_with_structure_impl(ctx))
859 }
860
861 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
862 ctx.content.is_empty() || ctx.list_blocks.is_empty()
865 }
866
867 fn category(&self) -> RuleCategory {
868 RuleCategory::List
869 }
870
871 fn as_any(&self) -> &dyn std::any::Any {
872 self
873 }
874
875 crate::impl_rule_config_methods!(MD032Config);
876}
877
878impl MD032BlanksAroundLists {
879 fn fix_with_structure_impl(&self, ctx: &crate::lint_context::LintContext) -> String {
881 let lines = ctx.raw_lines();
882 let num_lines = lines.len();
883 if num_lines == 0 {
884 return String::new();
885 }
886
887 let list_blocks = self.convert_list_blocks(ctx);
888 if list_blocks.is_empty() {
889 return ctx.content.to_string();
890 }
891
892 let mut lazy_fixes: std::collections::BTreeMap<usize, LazyContLine> = std::collections::BTreeMap::new();
895 if !self.config.allow_lazy_continuation {
896 let lazy_cont_lines = ctx.lazy_continuation_lines();
897 for lazy_info in lazy_cont_lines.iter() {
898 let line_num = lazy_info.line_num;
899 let is_within_block = list_blocks
901 .iter()
902 .any(|(start, end, _)| line_num >= *start && line_num <= *end);
903 if !is_within_block {
904 continue;
905 }
906 if !Self::should_apply_lazy_fix(ctx, line_num) {
908 continue;
909 }
910 lazy_fixes.insert(line_num, lazy_info.clone());
911 }
912 }
913
914 let mut insertions: std::collections::BTreeMap<usize, String> = std::collections::BTreeMap::new();
915
916 for &(start_line, end_line, ref prefix) in &list_blocks {
918 if ctx.inline_config().is_rule_disabled("MD032", start_line) {
920 continue;
921 }
922
923 if ctx
925 .line_info(start_line)
926 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
927 {
928 continue;
929 }
930
931 if start_line > 1 {
933 let (content_line, has_blank_separation) = Self::find_preceding_content(ctx, start_line);
935
936 if !has_blank_separation && content_line > 0 {
938 let prev_line_str = lines[content_line - 1];
939 let is_prev_excluded = ctx
940 .line_info(content_line)
941 .is_some_and(|info| info.in_code_block || info.in_front_matter);
942 let prev_prefix = BLOCKQUOTE_PREFIX_RE.find(prev_line_str).map_or("", |m| m.as_str());
943
944 let should_require = Self::should_require_blank_line_before(ctx, content_line, start_line);
945 if !is_prev_excluded && prev_prefix.trim() == prefix.trim() && should_require {
947 let bq_prefix = ctx.blockquote_prefix_for_blank_line(start_line - 1);
949 insertions.insert(start_line, bq_prefix);
950 }
951 }
952 }
953
954 if end_line < num_lines {
956 let (content_line, has_blank_separation) = Self::find_following_content(ctx, end_line);
958
959 if !has_blank_separation && content_line > 0 {
961 let next_line_str = lines[content_line - 1];
962 let is_next_excluded = ctx
964 .line_info(content_line)
965 .is_some_and(|info| info.in_code_block || info.in_front_matter)
966 || (content_line <= ctx.lines.len()
967 && ctx.lines[content_line - 1].in_code_block
968 && ctx.lines[content_line - 1].indent >= 2
969 && (ctx.lines[content_line - 1]
970 .content(ctx.content)
971 .trim()
972 .starts_with("```")
973 || ctx.lines[content_line - 1]
974 .content(ctx.content)
975 .trim()
976 .starts_with("~~~")));
977 let next_prefix = BLOCKQUOTE_PREFIX_RE.find(next_line_str).map_or("", |m| m.as_str());
978
979 let end_line_str = lines[end_line - 1];
981 let end_line_prefix = BLOCKQUOTE_PREFIX_RE.find(end_line_str).map_or("", |m| m.as_str());
982 let end_line_bq_level = end_line_prefix.chars().filter(|&c| c == '>').count();
983 let next_line_bq_level = next_prefix.chars().filter(|&c| c == '>').count();
984 let exits_blockquote = end_line_bq_level > 0 && next_line_bq_level < end_line_bq_level;
985
986 if !is_next_excluded && next_prefix.trim() == prefix.trim() && !exits_blockquote {
989 let bq_prefix = ctx.blockquote_prefix_for_blank_line(end_line - 1);
991 insertions.insert(end_line + 1, bq_prefix);
992 }
993 }
994 }
995 }
996
997 let mut result_lines: Vec<String> = Vec::with_capacity(num_lines + insertions.len());
999 for (i, line) in lines.iter().enumerate() {
1000 let current_line_num = i + 1;
1001 if let Some(prefix_to_insert) = insertions.get(¤t_line_num)
1002 && (result_lines.is_empty() || result_lines.last().unwrap() != prefix_to_insert)
1003 {
1004 result_lines.push(prefix_to_insert.clone());
1005 }
1006
1007 if let Some(lazy_info) = lazy_fixes.get(¤t_line_num)
1009 && !ctx.inline_config().is_rule_disabled("MD032", current_line_num)
1010 {
1011 let fixed_line = Self::apply_lazy_fix_to_line(line, lazy_info);
1012 result_lines.push(fixed_line);
1013 } else {
1014 result_lines.push(line.to_string());
1015 }
1016 }
1017
1018 let mut result = result_lines.join("\n");
1020 if ctx.content.ends_with('\n') {
1021 result.push('\n');
1022 }
1023 result
1024 }
1025}
1026
1027fn is_blank_in_context(line: &str) -> bool {
1029 if let Some(m) = BLOCKQUOTE_PREFIX_RE.find(line) {
1032 line[m.end()..].trim().is_empty()
1034 } else {
1035 line.trim().is_empty()
1037 }
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042 use super::*;
1043 use crate::lint_context::LintContext;
1044 use crate::rule::Rule;
1045
1046 fn lint(content: &str) -> Vec<LintWarning> {
1047 let rule = MD032BlanksAroundLists::default();
1048 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1049 rule.check(&ctx).expect("Lint check failed")
1050 }
1051
1052 fn fix(content: &str) -> String {
1053 let rule = MD032BlanksAroundLists::default();
1054 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1055 rule.fix(&ctx).expect("Lint fix failed")
1056 }
1057
1058 #[test]
1059 fn test_fix_does_not_split_item_before_different_list_type() {
1060 let content = "- alpha beta\n aligned\n1. ordered item\n cont\n";
1064 assert_eq!(fix(content), "- alpha beta\n aligned\n\n1. ordered item\n cont\n");
1065
1066 let warnings = lint(content);
1069 assert_eq!(warnings.len(), 2);
1070 assert_eq!(warnings[0].line, 2);
1071 assert_eq!(warnings[1].line, 3);
1072 }
1073
1074 #[test]
1075 fn test_fix_does_not_split_blockquoted_item_before_different_list_type() {
1076 let content = "> - alpha beta\n> aligned\n> 1. ordered item\n";
1077 assert_eq!(fix(content), "> - alpha beta\n> aligned\n>\n> 1. ordered item\n");
1078 }
1079
1080 #[test]
1081 fn test_fix_keeps_lazy_continuation_with_its_item() {
1082 let content = "- alpha beta\nlazy\n1. ordered item\n";
1086 assert_eq!(fix(content), "- alpha beta\nlazy\n\n1. ordered item\n");
1087
1088 let warnings = lint(content);
1089 assert_eq!(warnings.len(), 2);
1090 assert_eq!(warnings[0].line, 2);
1091 assert_eq!(warnings[1].line, 3);
1092 }
1093
1094 #[test]
1095 fn test_fix_keeps_blockquoted_lazy_continuation_with_its_item() {
1096 let content = "> - alpha beta\n> lazy\n> 1. ordered item\n";
1097 assert_eq!(fix(content), "> - alpha beta\n> lazy\n>\n> 1. ordered item\n");
1098 }
1099
1100 #[test]
1101 fn test_fix_indents_lazy_continuation_when_not_allowed() {
1102 let rule = MD032BlanksAroundLists::from_config_struct(MD032Config {
1105 allow_lazy_continuation: false,
1106 });
1107 let content = "- alpha beta\nlazy\n1. ordered item\n";
1108 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1109 let fixed = rule.fix(&ctx).expect("Lint fix failed");
1110 assert_eq!(fixed, "- alpha beta\n lazy\n\n1. ordered item\n");
1111 }
1112
1113 fn check_warnings_have_fixes(content: &str) {
1115 let warnings = lint(content);
1116 for warning in &warnings {
1117 assert!(warning.fix.is_some(), "Warning should have fix: {warning:?}");
1118 }
1119 }
1120
1121 #[test]
1122 fn test_list_at_start() {
1123 let content = "- Item 1\n- Item 2\nText";
1126 let warnings = lint(content);
1127 assert_eq!(
1128 warnings.len(),
1129 0,
1130 "Trailing text is lazy continuation per CommonMark - no warning expected"
1131 );
1132 }
1133
1134 #[test]
1135 fn test_list_at_end() {
1136 let content = "Text\n- Item 1\n- Item 2";
1137 let warnings = lint(content);
1138 assert_eq!(
1139 warnings.len(),
1140 1,
1141 "Expected 1 warning for list at end without preceding blank line"
1142 );
1143 assert_eq!(
1144 warnings[0].line, 2,
1145 "Warning should be on the first line of the list (line 2)"
1146 );
1147 assert!(warnings[0].message.contains("preceded by blank line"));
1148
1149 check_warnings_have_fixes(content);
1151
1152 let fixed_content = fix(content);
1153 assert_eq!(fixed_content, "Text\n\n- Item 1\n- Item 2");
1154
1155 let warnings_after_fix = lint(&fixed_content);
1157 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1158 }
1159
1160 #[test]
1161 fn test_list_in_middle() {
1162 let content = "Text 1\n- Item 1\n- Item 2\nText 2";
1165 let warnings = lint(content);
1166 assert_eq!(
1167 warnings.len(),
1168 1,
1169 "Expected 1 warning for list needing preceding blank line (trailing text is lazy continuation)"
1170 );
1171 assert_eq!(warnings[0].line, 2, "Warning on line 2 (start)");
1172 assert!(warnings[0].message.contains("preceded by blank line"));
1173
1174 check_warnings_have_fixes(content);
1176
1177 let fixed_content = fix(content);
1178 assert_eq!(fixed_content, "Text 1\n\n- Item 1\n- Item 2\nText 2");
1179
1180 let warnings_after_fix = lint(&fixed_content);
1182 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1183 }
1184
1185 #[test]
1186 fn test_correct_spacing() {
1187 let content = "Text 1\n\n- Item 1\n- Item 2\n\nText 2";
1188 let warnings = lint(content);
1189 assert_eq!(warnings.len(), 0, "Expected no warnings for correctly spaced list");
1190
1191 let fixed_content = fix(content);
1192 assert_eq!(fixed_content, content, "Fix should not change correctly spaced content");
1193 }
1194
1195 #[test]
1196 fn test_list_with_content() {
1197 let content = "Text\n* Item 1\n Content\n* Item 2\n More content\nText";
1200 let warnings = lint(content);
1201 assert_eq!(
1202 warnings.len(),
1203 1,
1204 "Expected 1 warning for list needing preceding blank line. Got: {warnings:?}"
1205 );
1206 assert_eq!(warnings[0].line, 2, "Warning should be on line 2 (start)");
1207 assert!(warnings[0].message.contains("preceded by blank line"));
1208
1209 check_warnings_have_fixes(content);
1211
1212 let fixed_content = fix(content);
1213 let expected_fixed = "Text\n\n* Item 1\n Content\n* Item 2\n More content\nText";
1214 assert_eq!(
1215 fixed_content, expected_fixed,
1216 "Fix did not produce the expected output. Got:\n{fixed_content}"
1217 );
1218
1219 let warnings_after_fix = lint(&fixed_content);
1221 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1222 }
1223
1224 #[test]
1225 fn test_nested_list() {
1226 let content = "Text\n- Item 1\n - Nested 1\n- Item 2\nText";
1228 let warnings = lint(content);
1229 assert_eq!(
1230 warnings.len(),
1231 1,
1232 "Nested list block needs preceding blank only. Got: {warnings:?}"
1233 );
1234 assert_eq!(warnings[0].line, 2);
1235 assert!(warnings[0].message.contains("preceded by blank line"));
1236
1237 check_warnings_have_fixes(content);
1239
1240 let fixed_content = fix(content);
1241 assert_eq!(fixed_content, "Text\n\n- Item 1\n - Nested 1\n- Item 2\nText");
1242
1243 let warnings_after_fix = lint(&fixed_content);
1245 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1246 }
1247
1248 #[test]
1249 fn test_list_with_internal_blanks() {
1250 let content = "Text\n* Item 1\n\n More Item 1 Content\n* Item 2\nText";
1252 let warnings = lint(content);
1253 assert_eq!(
1254 warnings.len(),
1255 1,
1256 "List with internal blanks needs preceding blank only. Got: {warnings:?}"
1257 );
1258 assert_eq!(warnings[0].line, 2);
1259 assert!(warnings[0].message.contains("preceded by blank line"));
1260
1261 check_warnings_have_fixes(content);
1263
1264 let fixed_content = fix(content);
1265 assert_eq!(
1266 fixed_content,
1267 "Text\n\n* Item 1\n\n More Item 1 Content\n* Item 2\nText"
1268 );
1269
1270 let warnings_after_fix = lint(&fixed_content);
1272 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1273 }
1274
1275 #[test]
1276 fn test_ignore_code_blocks() {
1277 let content = "```\n- Not a list item\n```\nText";
1278 let warnings = lint(content);
1279 assert_eq!(warnings.len(), 0);
1280 let fixed_content = fix(content);
1281 assert_eq!(fixed_content, content);
1282 }
1283
1284 #[test]
1285 fn test_ignore_front_matter() {
1286 let content = "---\ntitle: Test\n---\n- List Item\nText";
1288 let warnings = lint(content);
1289 assert_eq!(
1290 warnings.len(),
1291 0,
1292 "Front matter test should have no MD032 warnings. Got: {warnings:?}"
1293 );
1294
1295 let fixed_content = fix(content);
1297 assert_eq!(fixed_content, content, "No changes when no warnings");
1298 }
1299
1300 #[test]
1301 fn test_multiple_lists() {
1302 let content = "Text\n- List 1 Item 1\n- List 1 Item 2\nText 2\n* List 2 Item 1\nText 3";
1307 let warnings = lint(content);
1308 assert!(
1310 !warnings.is_empty(),
1311 "Should have at least one warning for missing blank line. Got: {warnings:?}"
1312 );
1313
1314 check_warnings_have_fixes(content);
1316
1317 let fixed_content = fix(content);
1318 let warnings_after_fix = lint(&fixed_content);
1320 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1321 }
1322
1323 #[test]
1324 fn test_adjacent_lists() {
1325 let content = "- List 1\n\n* List 2";
1326 let warnings = lint(content);
1327 assert_eq!(warnings.len(), 0);
1328 let fixed_content = fix(content);
1329 assert_eq!(fixed_content, content);
1330 }
1331
1332 #[test]
1333 fn test_list_in_blockquote() {
1334 let content = "> Quote line 1\n> - List item 1\n> - List item 2\n> Quote line 2";
1336 let warnings = lint(content);
1337 assert_eq!(
1338 warnings.len(),
1339 1,
1340 "Expected 1 warning for blockquoted list needing preceding blank. Got: {warnings:?}"
1341 );
1342 assert_eq!(warnings[0].line, 2);
1343
1344 check_warnings_have_fixes(content);
1346
1347 let fixed_content = fix(content);
1348 assert_eq!(
1350 fixed_content, "> Quote line 1\n>\n> - List item 1\n> - List item 2\n> Quote line 2",
1351 "Fix for blockquoted list failed. Got:\n{fixed_content}"
1352 );
1353
1354 let warnings_after_fix = lint(&fixed_content);
1356 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1357 }
1358
1359 #[test]
1360 fn test_ordered_list() {
1361 let content = "Text\n1. Item 1\n2. Item 2\nText";
1363 let warnings = lint(content);
1364 assert_eq!(warnings.len(), 1);
1365
1366 check_warnings_have_fixes(content);
1368
1369 let fixed_content = fix(content);
1370 assert_eq!(fixed_content, "Text\n\n1. Item 1\n2. Item 2\nText");
1371
1372 let warnings_after_fix = lint(&fixed_content);
1374 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1375 }
1376
1377 #[test]
1378 fn test_no_double_blank_fix() {
1379 let content = "Text\n\n- Item 1\n- Item 2\nText"; let warnings = lint(content);
1382 assert_eq!(
1383 warnings.len(),
1384 0,
1385 "Should have no warnings - properly preceded, trailing is lazy"
1386 );
1387
1388 let fixed_content = fix(content);
1389 assert_eq!(
1390 fixed_content, content,
1391 "No fix needed when no warnings. Got:\n{fixed_content}"
1392 );
1393
1394 let content2 = "Text\n- Item 1\n- Item 2\n\nText"; let warnings2 = lint(content2);
1396 assert_eq!(warnings2.len(), 1);
1397 if !warnings2.is_empty() {
1398 assert_eq!(
1399 warnings2[0].line, 2,
1400 "Warning line for missing blank before should be the first line of the block"
1401 );
1402 }
1403
1404 check_warnings_have_fixes(content2);
1406
1407 let fixed_content2 = fix(content2);
1408 assert_eq!(
1409 fixed_content2, "Text\n\n- Item 1\n- Item 2\n\nText",
1410 "Fix added extra blank before. Got:\n{fixed_content2}"
1411 );
1412 }
1413
1414 #[test]
1415 fn test_empty_input() {
1416 let content = "";
1417 let warnings = lint(content);
1418 assert_eq!(warnings.len(), 0);
1419 let fixed_content = fix(content);
1420 assert_eq!(fixed_content, "");
1421 }
1422
1423 #[test]
1424 fn test_only_list() {
1425 let content = "- Item 1\n- Item 2";
1426 let warnings = lint(content);
1427 assert_eq!(warnings.len(), 0);
1428 let fixed_content = fix(content);
1429 assert_eq!(fixed_content, content);
1430 }
1431
1432 #[test]
1435 fn test_fix_complex_nested_blockquote() {
1436 let content = "> Text before\n> - Item 1\n> - Nested item\n> - Item 2\n> Text after";
1438 let warnings = lint(content);
1439 assert_eq!(
1440 warnings.len(),
1441 1,
1442 "Should warn for missing preceding blank only. Got: {warnings:?}"
1443 );
1444
1445 check_warnings_have_fixes(content);
1447
1448 let fixed_content = fix(content);
1449 let expected = "> Text before\n>\n> - Item 1\n> - Nested item\n> - Item 2\n> Text after";
1451 assert_eq!(fixed_content, expected, "Fix should preserve blockquote structure");
1452
1453 let warnings_after_fix = lint(&fixed_content);
1454 assert_eq!(warnings_after_fix.len(), 0, "Fix should eliminate all warnings");
1455 }
1456
1457 #[test]
1458 fn test_fix_mixed_list_markers() {
1459 let content = "Text\n- Item 1\n* Item 2\n+ Item 3\nText";
1462 let warnings = lint(content);
1463 assert!(
1465 !warnings.is_empty(),
1466 "Should have at least 1 warning for mixed marker list. Got: {warnings:?}"
1467 );
1468
1469 check_warnings_have_fixes(content);
1471
1472 let fixed_content = fix(content);
1473 assert!(
1475 fixed_content.contains("Text\n\n-"),
1476 "Fix should add blank line before first list item"
1477 );
1478
1479 let warnings_after_fix = lint(&fixed_content);
1481 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1482 }
1483
1484 #[test]
1485 fn test_fix_ordered_list_with_different_numbers() {
1486 let content = "Text\n1. First\n3. Third\n2. Second\nText";
1488 let warnings = lint(content);
1489 assert_eq!(warnings.len(), 1, "Should warn for missing preceding blank only");
1490
1491 check_warnings_have_fixes(content);
1493
1494 let fixed_content = fix(content);
1495 let expected = "Text\n\n1. First\n3. Third\n2. Second\nText";
1496 assert_eq!(
1497 fixed_content, expected,
1498 "Fix should handle ordered lists with non-sequential numbers"
1499 );
1500
1501 let warnings_after_fix = lint(&fixed_content);
1503 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1504 }
1505
1506 #[test]
1507 fn test_fix_list_with_code_blocks_inside() {
1508 let content = "Text\n- Item 1\n ```\n code\n ```\n- Item 2\nText";
1510 let warnings = lint(content);
1511 assert_eq!(warnings.len(), 1, "Should warn for missing preceding blank only");
1512
1513 check_warnings_have_fixes(content);
1515
1516 let fixed_content = fix(content);
1517 let expected = "Text\n\n- Item 1\n ```\n code\n ```\n- Item 2\nText";
1518 assert_eq!(
1519 fixed_content, expected,
1520 "Fix should handle lists with internal code blocks"
1521 );
1522
1523 let warnings_after_fix = lint(&fixed_content);
1525 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1526 }
1527
1528 #[test]
1529 fn test_fix_deeply_nested_lists() {
1530 let content = "Text\n- Level 1\n - Level 2\n - Level 3\n - Level 4\n- Back to Level 1\nText";
1532 let warnings = lint(content);
1533 assert_eq!(warnings.len(), 1, "Should warn for missing preceding blank only");
1534
1535 check_warnings_have_fixes(content);
1537
1538 let fixed_content = fix(content);
1539 let expected = "Text\n\n- Level 1\n - Level 2\n - Level 3\n - Level 4\n- Back to Level 1\nText";
1540 assert_eq!(fixed_content, expected, "Fix should handle deeply nested lists");
1541
1542 let warnings_after_fix = lint(&fixed_content);
1544 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1545 }
1546
1547 #[test]
1548 fn test_fix_list_with_multiline_items() {
1549 let content = "Text\n- Item 1\n continues here\n and here\n- Item 2\n also continues\nText";
1552 let warnings = lint(content);
1553 assert_eq!(
1554 warnings.len(),
1555 1,
1556 "Should only warn for missing blank before list (trailing text is lazy continuation)"
1557 );
1558
1559 check_warnings_have_fixes(content);
1561
1562 let fixed_content = fix(content);
1563 let expected = "Text\n\n- Item 1\n continues here\n and here\n- Item 2\n also continues\nText";
1564 assert_eq!(fixed_content, expected, "Fix should add blank before list only");
1565
1566 let warnings_after_fix = lint(&fixed_content);
1568 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1569 }
1570
1571 #[test]
1572 fn test_fix_list_at_document_boundaries() {
1573 let content1 = "- Item 1\n- Item 2";
1575 let warnings1 = lint(content1);
1576 assert_eq!(
1577 warnings1.len(),
1578 0,
1579 "List at document start should not need blank before"
1580 );
1581 let fixed1 = fix(content1);
1582 assert_eq!(fixed1, content1, "No fix needed for list at start");
1583
1584 let content2 = "Text\n- Item 1\n- Item 2";
1586 let warnings2 = lint(content2);
1587 assert_eq!(warnings2.len(), 1, "List at document end should need blank before");
1588 check_warnings_have_fixes(content2);
1589 let fixed2 = fix(content2);
1590 assert_eq!(
1591 fixed2, "Text\n\n- Item 1\n- Item 2",
1592 "Should add blank before list at end"
1593 );
1594 }
1595
1596 #[test]
1597 fn test_fix_preserves_existing_blank_lines() {
1598 let content = "Text\n\n\n- Item 1\n- Item 2\n\n\nText";
1599 let warnings = lint(content);
1600 assert_eq!(warnings.len(), 0, "Multiple blank lines should be preserved");
1601 let fixed_content = fix(content);
1602 assert_eq!(fixed_content, content, "Fix should not modify already correct content");
1603 }
1604
1605 #[test]
1606 fn test_fix_handles_tabs_and_spaces() {
1607 let content = "Text\n\t- Item with tab\n - Item with spaces\nText";
1610 let warnings = lint(content);
1611 assert!(!warnings.is_empty(), "Should warn for missing blank before list");
1613
1614 check_warnings_have_fixes(content);
1616
1617 let fixed_content = fix(content);
1618 let expected = "Text\n\t- Item with tab\n\n - Item with spaces\nText";
1621 assert_eq!(fixed_content, expected, "Fix should add blank before list item");
1622
1623 let warnings_after_fix = lint(&fixed_content);
1625 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1626 }
1627
1628 #[test]
1629 fn test_fix_warning_objects_have_correct_ranges() {
1630 let content = "Text\n- Item 1\n- Item 2\nText";
1632 let warnings = lint(content);
1633 assert_eq!(warnings.len(), 1, "Only preceding blank warning expected");
1634
1635 for warning in &warnings {
1637 assert!(warning.fix.is_some(), "Warning should have fix");
1638 let fix = warning.fix.as_ref().unwrap();
1639 assert!(fix.range.start <= fix.range.end, "Fix range should be valid");
1640 assert!(
1641 !fix.replacement.is_empty() || fix.range.start == fix.range.end,
1642 "Fix should have replacement or be insertion"
1643 );
1644 }
1645 }
1646
1647 #[test]
1648 fn test_fix_idempotent() {
1649 let content = "Text\n- Item 1\n- Item 2\nText";
1651
1652 let fixed_once = fix(content);
1654 assert_eq!(fixed_once, "Text\n\n- Item 1\n- Item 2\nText");
1655
1656 let fixed_twice = fix(&fixed_once);
1658 assert_eq!(fixed_twice, fixed_once, "Fix should be idempotent");
1659
1660 let warnings_after_fix = lint(&fixed_once);
1662 assert_eq!(warnings_after_fix.len(), 0, "No warnings should remain after fix");
1663 }
1664
1665 #[test]
1666 fn test_fix_with_normalized_line_endings() {
1667 let content = "Text\n- Item 1\n- Item 2\nText";
1671 let warnings = lint(content);
1672 assert_eq!(warnings.len(), 1, "Should detect missing blank before list");
1673
1674 check_warnings_have_fixes(content);
1676
1677 let fixed_content = fix(content);
1678 let expected = "Text\n\n- Item 1\n- Item 2\nText";
1680 assert_eq!(fixed_content, expected, "Fix should work with normalized LF content");
1681 }
1682
1683 #[test]
1684 fn test_fix_preserves_final_newline() {
1685 let content_with_newline = "Text\n- Item 1\n- Item 2\nText\n";
1688 let fixed_with_newline = fix(content_with_newline);
1689 assert!(
1690 fixed_with_newline.ends_with('\n'),
1691 "Fix should preserve final newline when present"
1692 );
1693 assert_eq!(fixed_with_newline, "Text\n\n- Item 1\n- Item 2\nText\n");
1695
1696 let content_without_newline = "Text\n- Item 1\n- Item 2\nText";
1698 let fixed_without_newline = fix(content_without_newline);
1699 assert!(
1700 !fixed_without_newline.ends_with('\n'),
1701 "Fix should not add final newline when not present"
1702 );
1703 assert_eq!(fixed_without_newline, "Text\n\n- Item 1\n- Item 2\nText");
1705 }
1706
1707 #[test]
1708 fn test_fix_multiline_list_items_no_indent() {
1709 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";
1710
1711 let warnings = lint(content);
1712 assert_eq!(
1714 warnings.len(),
1715 0,
1716 "Should not warn for properly formatted list with multi-line items. Got: {warnings:?}"
1717 );
1718
1719 let fixed_content = fix(content);
1720 assert_eq!(
1722 fixed_content, content,
1723 "Should not modify correctly formatted multi-line list items"
1724 );
1725 }
1726
1727 #[test]
1728 fn test_nested_list_with_lazy_continuation() {
1729 let content = r#"# Test
1735
1736- **Token Dispatch (Phase 3.2)**: COMPLETE. Extracts tokens from both:
1737 1. Switch/case dispatcher statements (original Phase 3.2)
1738 2. Inline conditionals - if/else, bitwise checks (`&`, `|`), comparison (`==`,
1739`!=`), ternary operators (`?:`), macros (`ISTOK`, `ISUNSET`), compound conditions (`&&`, `||`) (Phase 3.2.1)
1740 - 30 explicit tokens extracted, 23 dispatcher rules with embedded token
1741 references"#;
1742
1743 let warnings = lint(content);
1744 let md032_warnings: Vec<_> = warnings
1747 .iter()
1748 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1749 .collect();
1750 assert_eq!(
1751 md032_warnings.len(),
1752 0,
1753 "Should not warn for nested list with lazy continuation. Got: {md032_warnings:?}"
1754 );
1755 }
1756
1757 #[test]
1758 fn test_pipes_in_code_spans_not_detected_as_table() {
1759 let content = r#"# Test
1761
1762- Item with `a | b` inline code
1763 - Nested item should work
1764
1765"#;
1766
1767 let warnings = lint(content);
1768 let md032_warnings: Vec<_> = warnings
1769 .iter()
1770 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1771 .collect();
1772 assert_eq!(
1773 md032_warnings.len(),
1774 0,
1775 "Pipes in code spans should not break lists. Got: {md032_warnings:?}"
1776 );
1777 }
1778
1779 #[test]
1780 fn test_multiple_code_spans_with_pipes() {
1781 let content = r#"# Test
1783
1784- Item with `a | b` and `c || d` operators
1785 - Nested item should work
1786
1787"#;
1788
1789 let warnings = lint(content);
1790 let md032_warnings: Vec<_> = warnings
1791 .iter()
1792 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1793 .collect();
1794 assert_eq!(
1795 md032_warnings.len(),
1796 0,
1797 "Multiple code spans with pipes should not break lists. Got: {md032_warnings:?}"
1798 );
1799 }
1800
1801 #[test]
1802 fn test_actual_table_breaks_list() {
1803 let content = r#"# Test
1805
1806- Item before table
1807
1808| Col1 | Col2 |
1809|------|------|
1810| A | B |
1811
1812- Item after table
1813
1814"#;
1815
1816 let warnings = lint(content);
1817 let md032_warnings: Vec<_> = warnings
1819 .iter()
1820 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1821 .collect();
1822 assert_eq!(
1823 md032_warnings.len(),
1824 0,
1825 "Both lists should be properly separated by blank lines. Got: {md032_warnings:?}"
1826 );
1827 }
1828
1829 #[test]
1830 fn test_thematic_break_not_lazy_continuation() {
1831 let content = r#"- Item 1
1834- Item 2
1835***
1836
1837More text.
1838"#;
1839
1840 let warnings = lint(content);
1841 let md032_warnings: Vec<_> = warnings
1842 .iter()
1843 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1844 .collect();
1845 assert_eq!(
1846 md032_warnings.len(),
1847 1,
1848 "Should warn for list not followed by blank line before thematic break. Got: {md032_warnings:?}"
1849 );
1850 assert!(
1851 md032_warnings[0].message.contains("followed by blank line"),
1852 "Warning should be about missing blank after list"
1853 );
1854 }
1855
1856 #[test]
1857 fn test_thematic_break_with_blank_line() {
1858 let content = r#"- Item 1
1860- Item 2
1861
1862***
1863
1864More text.
1865"#;
1866
1867 let warnings = lint(content);
1868 let md032_warnings: Vec<_> = warnings
1869 .iter()
1870 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1871 .collect();
1872 assert_eq!(
1873 md032_warnings.len(),
1874 0,
1875 "Should not warn when list is properly followed by blank line. Got: {md032_warnings:?}"
1876 );
1877 }
1878
1879 #[test]
1880 fn test_various_thematic_break_styles() {
1881 for hr in ["---", "***", "___"] {
1886 let content = format!(
1887 r#"- Item 1
1888- Item 2
1889{hr}
1890
1891More text.
1892"#
1893 );
1894
1895 let warnings = lint(&content);
1896 let md032_warnings: Vec<_> = warnings
1897 .iter()
1898 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1899 .collect();
1900 assert_eq!(
1901 md032_warnings.len(),
1902 1,
1903 "Should warn for HR style '{hr}' without blank line. Got: {md032_warnings:?}"
1904 );
1905 }
1906 }
1907
1908 fn lint_with_config(content: &str, config: MD032Config) -> Vec<LintWarning> {
1911 let rule = MD032BlanksAroundLists::from_config_struct(config);
1912 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1913 rule.check(&ctx).expect("Lint check failed")
1914 }
1915
1916 fn fix_with_config(content: &str, config: MD032Config) -> String {
1917 let rule = MD032BlanksAroundLists::from_config_struct(config);
1918 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1919 rule.fix(&ctx).expect("Lint fix failed")
1920 }
1921
1922 #[test]
1923 fn test_lazy_continuation_allowed_by_default() {
1924 let content = "# Heading\n\n1. List\nSome text.";
1926 let warnings = lint(content);
1927 assert_eq!(
1928 warnings.len(),
1929 0,
1930 "Default behavior should allow lazy continuation. Got: {warnings:?}"
1931 );
1932 }
1933
1934 #[test]
1935 fn test_lazy_continuation_disallowed() {
1936 let content = "# Heading\n\n1. List\nSome text.";
1938 let config = MD032Config {
1939 allow_lazy_continuation: false,
1940 };
1941 let warnings = lint_with_config(content, config);
1942 assert_eq!(
1943 warnings.len(),
1944 1,
1945 "Should warn when lazy continuation is disallowed. Got: {warnings:?}"
1946 );
1947 assert!(
1948 warnings[0].message.contains("Lazy continuation"),
1949 "Warning message should mention lazy continuation"
1950 );
1951 assert_eq!(warnings[0].line, 4, "Warning should be on the lazy line");
1952 }
1953
1954 #[test]
1955 fn test_lazy_continuation_fix() {
1956 let content = "# Heading\n\n1. List\nSome text.";
1958 let config = MD032Config {
1959 allow_lazy_continuation: false,
1960 };
1961 let fixed = fix_with_config(content, config.clone());
1962 assert_eq!(
1964 fixed, "# Heading\n\n1. List\n Some text.",
1965 "Fix should add proper indentation to lazy continuation"
1966 );
1967
1968 let warnings_after = lint_with_config(&fixed, config);
1970 assert_eq!(warnings_after.len(), 0, "No warnings should remain after fix");
1971 }
1972
1973 #[test]
1974 fn test_lazy_continuation_multiple_lines() {
1975 let content = "- Item 1\nLine 2\nLine 3";
1977 let config = MD032Config {
1978 allow_lazy_continuation: false,
1979 };
1980 let warnings = lint_with_config(content, config.clone());
1981 assert_eq!(
1983 warnings.len(),
1984 2,
1985 "Should warn for each lazy continuation line. Got: {warnings:?}"
1986 );
1987
1988 let fixed = fix_with_config(content, config.clone());
1989 assert_eq!(
1991 fixed, "- Item 1\n Line 2\n Line 3",
1992 "Fix should add proper indentation to lazy continuation lines"
1993 );
1994
1995 let warnings_after = lint_with_config(&fixed, config);
1997 assert_eq!(warnings_after.len(), 0, "No warnings should remain after fix");
1998 }
1999
2000 #[test]
2001 fn test_lazy_continuation_with_indented_content() {
2002 let content = "- Item 1\n Indented content\nLazy text";
2004 let config = MD032Config {
2005 allow_lazy_continuation: false,
2006 };
2007 let warnings = lint_with_config(content, config);
2008 assert_eq!(
2009 warnings.len(),
2010 1,
2011 "Should warn for lazy text after indented content. Got: {warnings:?}"
2012 );
2013 }
2014
2015 #[test]
2016 fn test_lazy_continuation_properly_separated() {
2017 let content = "- Item 1\n\nSome text.";
2019 let config = MD032Config {
2020 allow_lazy_continuation: false,
2021 };
2022 let warnings = lint_with_config(content, config);
2023 assert_eq!(
2024 warnings.len(),
2025 0,
2026 "Should not warn when list is properly followed by blank line. Got: {warnings:?}"
2027 );
2028 }
2029
2030 #[test]
2033 fn test_lazy_continuation_ordered_list_parenthesis_marker() {
2034 let content = "1) First item\nLazy continuation";
2036 let config = MD032Config {
2037 allow_lazy_continuation: false,
2038 };
2039 let warnings = lint_with_config(content, config.clone());
2040 assert_eq!(
2041 warnings.len(),
2042 1,
2043 "Should warn for lazy continuation with parenthesis marker"
2044 );
2045
2046 let fixed = fix_with_config(content, config);
2047 assert_eq!(fixed, "1) First item\n Lazy continuation");
2049 }
2050
2051 #[test]
2052 fn test_lazy_continuation_followed_by_another_list() {
2053 let content = "- Item 1\nSome text\n- Item 2";
2059 let config = MD032Config {
2060 allow_lazy_continuation: false,
2061 };
2062 let warnings = lint_with_config(content, config);
2063 assert_eq!(
2065 warnings.len(),
2066 1,
2067 "Should warn about lazy continuation within list. Got: {warnings:?}"
2068 );
2069 assert!(
2070 warnings[0].message.contains("Lazy continuation"),
2071 "Warning should be about lazy continuation"
2072 );
2073 assert_eq!(warnings[0].line, 2, "Warning should be on line 2");
2074 }
2075
2076 #[test]
2077 fn test_lazy_continuation_multiple_in_document() {
2078 let content = "- Item 1\nLazy 1\n\n- Item 2\nLazy 2";
2083 let config = MD032Config {
2084 allow_lazy_continuation: false,
2085 };
2086 let warnings = lint_with_config(content, config.clone());
2087 assert_eq!(
2089 warnings.len(),
2090 2,
2091 "Should warn for both lazy continuations. Got: {warnings:?}"
2092 );
2093
2094 let fixed = fix_with_config(content, config.clone());
2095 assert!(
2097 fixed.contains(" Lazy 1"),
2098 "Fixed content should have indented 'Lazy 1'. Got: {fixed:?}"
2099 );
2100 assert!(
2101 fixed.contains(" Lazy 2"),
2102 "Fixed content should have indented 'Lazy 2'. Got: {fixed:?}"
2103 );
2104
2105 let warnings_after = lint_with_config(&fixed, config);
2106 assert_eq!(
2108 warnings_after.len(),
2109 0,
2110 "All warnings should be fixed after auto-fix. Got: {warnings_after:?}"
2111 );
2112 }
2113
2114 #[test]
2115 fn test_lazy_continuation_end_of_document_no_newline() {
2116 let content = "- Item\nNo trailing newline";
2118 let config = MD032Config {
2119 allow_lazy_continuation: false,
2120 };
2121 let warnings = lint_with_config(content, config.clone());
2122 assert_eq!(warnings.len(), 1, "Should warn even at end of document");
2123
2124 let fixed = fix_with_config(content, config);
2125 assert_eq!(fixed, "- Item\n No trailing newline");
2127 }
2128
2129 #[test]
2130 fn test_lazy_continuation_thematic_break_still_needs_blank() {
2131 let content = "- Item 1\n---";
2134 let config = MD032Config {
2135 allow_lazy_continuation: false,
2136 };
2137 let warnings = lint_with_config(content, config.clone());
2138 assert_eq!(
2140 warnings.len(),
2141 1,
2142 "List should need blank line before thematic break. Got: {warnings:?}"
2143 );
2144
2145 let fixed = fix_with_config(content, config);
2147 assert_eq!(fixed, "- Item 1\n\n---");
2148 }
2149
2150 #[test]
2151 fn test_lazy_continuation_heading_not_flagged() {
2152 let content = "- Item 1\n# Heading";
2155 let config = MD032Config {
2156 allow_lazy_continuation: false,
2157 };
2158 let warnings = lint_with_config(content, config);
2159 assert!(
2162 warnings.iter().all(|w| !w.message.contains("lazy")),
2163 "Heading should not trigger lazy continuation warning"
2164 );
2165 }
2166
2167 #[test]
2168 fn test_lazy_continuation_mixed_list_types() {
2169 let content = "- Unordered\n1. Ordered\nLazy text";
2171 let config = MD032Config {
2172 allow_lazy_continuation: false,
2173 };
2174 let warnings = lint_with_config(content, config.clone());
2175 assert!(!warnings.is_empty(), "Should warn about structure issues");
2176 }
2177
2178 #[test]
2179 fn test_lazy_continuation_deep_nesting() {
2180 let content = "- Level 1\n - Level 2\n - Level 3\nLazy at root";
2182 let config = MD032Config {
2183 allow_lazy_continuation: false,
2184 };
2185 let warnings = lint_with_config(content, config.clone());
2186 assert!(
2187 !warnings.is_empty(),
2188 "Should warn about lazy continuation after nested list"
2189 );
2190
2191 let fixed = fix_with_config(content, config.clone());
2192 let warnings_after = lint_with_config(&fixed, config);
2193 assert_eq!(warnings_after.len(), 0, "No warnings should remain after fix");
2194 }
2195
2196 #[test]
2197 fn test_lazy_continuation_with_emphasis_in_text() {
2198 let content = "- Item\n*emphasized* continuation";
2200 let config = MD032Config {
2201 allow_lazy_continuation: false,
2202 };
2203 let warnings = lint_with_config(content, config.clone());
2204 assert_eq!(warnings.len(), 1, "Should warn even with emphasis in continuation");
2205
2206 let fixed = fix_with_config(content, config);
2207 assert_eq!(fixed, "- Item\n *emphasized* continuation");
2209 }
2210
2211 #[test]
2212 fn test_lazy_continuation_with_code_span() {
2213 let content = "- Item\n`code` continuation";
2215 let config = MD032Config {
2216 allow_lazy_continuation: false,
2217 };
2218 let warnings = lint_with_config(content, config.clone());
2219 assert_eq!(warnings.len(), 1, "Should warn even with code span in continuation");
2220
2221 let fixed = fix_with_config(content, config);
2222 assert_eq!(fixed, "- Item\n `code` continuation");
2224 }
2225
2226 #[test]
2233 fn test_issue295_case1_nested_bullets_then_continuation_then_item() {
2234 let content = r#"1. Create a new Chat conversation:
2237 - On the sidebar, select **New Chat**.
2238 - In the box, type `/new`.
2239 A new Chat conversation replaces the previous one.
22401. Under the Chat text box, turn off the toggle."#;
2241 let config = MD032Config {
2242 allow_lazy_continuation: false,
2243 };
2244 let warnings = lint_with_config(content, config);
2245 let lazy_warnings: Vec<_> = warnings
2247 .iter()
2248 .filter(|w| w.message.contains("Lazy continuation"))
2249 .collect();
2250 assert!(
2251 !lazy_warnings.is_empty(),
2252 "Should detect lazy continuation after nested bullets. Got: {warnings:?}"
2253 );
2254 assert!(
2255 lazy_warnings.iter().any(|w| w.line == 4),
2256 "Should warn on line 4. Got: {lazy_warnings:?}"
2257 );
2258 }
2259
2260 #[test]
2261 fn test_issue295_case3_code_span_starts_lazy_continuation() {
2262 let content = r#"- `field`: Is the specific key:
2265 - `password`: Accesses the password.
2266 - `api_key`: Accesses the api_key.
2267 `token`: Specifies which ID token to use.
2268- `version_id`: Is the unique identifier."#;
2269 let config = MD032Config {
2270 allow_lazy_continuation: false,
2271 };
2272 let warnings = lint_with_config(content, config);
2273 let lazy_warnings: Vec<_> = warnings
2275 .iter()
2276 .filter(|w| w.message.contains("Lazy continuation"))
2277 .collect();
2278 assert!(
2279 !lazy_warnings.is_empty(),
2280 "Should detect lazy continuation starting with code span. Got: {warnings:?}"
2281 );
2282 assert!(
2283 lazy_warnings.iter().any(|w| w.line == 4),
2284 "Should warn on line 4 (code span start). Got: {lazy_warnings:?}"
2285 );
2286 }
2287
2288 #[test]
2289 fn test_issue295_case4_deep_nesting_with_continuation_then_item() {
2290 let content = r#"- Check out the branch, and test locally.
2292 - If the MR requires significant modifications:
2293 - **Skip local testing** and review instead.
2294 - **Request verification** from the author.
2295 - **Identify the minimal change** needed.
2296 Your testing might result in opportunities.
2297- If you don't understand, _say so_."#;
2298 let config = MD032Config {
2299 allow_lazy_continuation: false,
2300 };
2301 let warnings = lint_with_config(content, config);
2302 let lazy_warnings: Vec<_> = warnings
2304 .iter()
2305 .filter(|w| w.message.contains("Lazy continuation"))
2306 .collect();
2307 assert!(
2308 !lazy_warnings.is_empty(),
2309 "Should detect lazy continuation after deep nesting. Got: {warnings:?}"
2310 );
2311 assert!(
2312 lazy_warnings.iter().any(|w| w.line == 6),
2313 "Should warn on line 6. Got: {lazy_warnings:?}"
2314 );
2315 }
2316
2317 #[test]
2318 fn test_issue295_ordered_list_nested_bullets_continuation() {
2319 let content = r#"# Test
2322
23231. First item.
2324 - Nested A.
2325 - Nested B.
2326 Continuation at outer level.
23271. Second item."#;
2328 let config = MD032Config {
2329 allow_lazy_continuation: false,
2330 };
2331 let warnings = lint_with_config(content, config);
2332 let lazy_warnings: Vec<_> = warnings
2334 .iter()
2335 .filter(|w| w.message.contains("Lazy continuation"))
2336 .collect();
2337 assert!(
2338 !lazy_warnings.is_empty(),
2339 "Should detect lazy continuation at outer level after nested. Got: {warnings:?}"
2340 );
2341 assert!(
2343 lazy_warnings.iter().any(|w| w.line == 6),
2344 "Should warn on line 6. Got: {lazy_warnings:?}"
2345 );
2346 }
2347
2348 #[test]
2349 fn test_issue295_multiple_lazy_lines_after_nested() {
2350 let content = r#"1. The device client receives a response.
2352 - Those defined by OAuth Framework.
2353 - Those specific to device authorization.
2354 Those error responses are described below.
2355 For more information on each response,
2356 see the documentation.
23571. Next step in the process."#;
2358 let config = MD032Config {
2359 allow_lazy_continuation: false,
2360 };
2361 let warnings = lint_with_config(content, config);
2362 let lazy_warnings: Vec<_> = warnings
2364 .iter()
2365 .filter(|w| w.message.contains("Lazy continuation"))
2366 .collect();
2367 assert!(
2368 lazy_warnings.len() >= 3,
2369 "Should detect multiple lazy continuation lines. Got {} warnings: {lazy_warnings:?}",
2370 lazy_warnings.len()
2371 );
2372 }
2373
2374 #[test]
2375 fn test_issue295_properly_indented_not_lazy() {
2376 let content = r#"1. First item.
2378 - Nested A.
2379 - Nested B.
2380
2381 Properly indented continuation.
23821. Second item."#;
2383 let config = MD032Config {
2384 allow_lazy_continuation: false,
2385 };
2386 let warnings = lint_with_config(content, config);
2387 let lazy_warnings: Vec<_> = warnings
2389 .iter()
2390 .filter(|w| w.message.contains("Lazy continuation"))
2391 .collect();
2392 assert_eq!(
2393 lazy_warnings.len(),
2394 0,
2395 "Should NOT warn when blank line separates continuation. Got: {lazy_warnings:?}"
2396 );
2397 }
2398
2399 #[test]
2406 fn test_html_comment_before_list_with_preceding_blank() {
2407 let content = "Some text.\n\n<!-- comment -->\n- List item";
2410 let warnings = lint(content);
2411 assert_eq!(
2412 warnings.len(),
2413 0,
2414 "Should not warn when blank line exists before HTML comment. Got: {warnings:?}"
2415 );
2416 }
2417
2418 #[test]
2419 fn test_html_comment_after_list_with_following_blank() {
2420 let content = "- List item\n<!-- comment -->\n\nSome text.";
2422 let warnings = lint(content);
2423 assert_eq!(
2424 warnings.len(),
2425 0,
2426 "Should not warn when blank line exists after HTML comment. Got: {warnings:?}"
2427 );
2428 }
2429
2430 #[test]
2431 fn test_list_inside_html_comment_ignored() {
2432 let content = "<!--\n1. First\n2. Second\n3. Third\n-->";
2434 let warnings = lint(content);
2435 assert_eq!(
2436 warnings.len(),
2437 0,
2438 "Should not analyze lists inside HTML comments. Got: {warnings:?}"
2439 );
2440 }
2441
2442 #[test]
2443 fn test_multiline_html_comment_before_list() {
2444 let content = "Text\n\n<!--\nThis is a\nmulti-line\ncomment\n-->\n- Item";
2446 let warnings = lint(content);
2447 assert_eq!(
2448 warnings.len(),
2449 0,
2450 "Multi-line HTML comment should be transparent. Got: {warnings:?}"
2451 );
2452 }
2453
2454 #[test]
2455 fn test_no_blank_before_html_comment_still_warns() {
2456 let content = "Some text.\n<!-- comment -->\n- List item";
2458 let warnings = lint(content);
2459 assert_eq!(
2460 warnings.len(),
2461 1,
2462 "Should warn when no blank line exists (even with HTML comment). Got: {warnings:?}"
2463 );
2464 assert!(
2465 warnings[0].message.contains("preceded by blank line"),
2466 "Should be 'preceded by blank line' warning"
2467 );
2468 }
2469
2470 #[test]
2471 fn test_no_blank_after_html_comment_no_warn_lazy_continuation() {
2472 let content = "- List item\n<!-- comment -->\nSome text.";
2475 let warnings = lint(content);
2476 assert_eq!(
2477 warnings.len(),
2478 0,
2479 "Should not warn - text after comment becomes lazy continuation. Got: {warnings:?}"
2480 );
2481 }
2482
2483 #[test]
2484 fn test_list_followed_by_heading_through_comment_should_warn() {
2485 let content = "- List item\n<!-- comment -->\n# Heading";
2487 let warnings = lint(content);
2488 assert!(
2491 warnings.len() <= 1,
2492 "Should handle heading after comment gracefully. Got: {warnings:?}"
2493 );
2494 }
2495
2496 #[test]
2497 fn test_html_comment_between_list_and_text_both_directions() {
2498 let content = "Text before.\n\n<!-- comment -->\n- Item 1\n- Item 2\n<!-- another -->\n\nText after.";
2500 let warnings = lint(content);
2501 assert_eq!(
2502 warnings.len(),
2503 0,
2504 "Should not warn with proper separation through comments. Got: {warnings:?}"
2505 );
2506 }
2507
2508 #[test]
2509 fn test_html_comment_fix_does_not_insert_unnecessary_blank() {
2510 let content = "Text.\n\n<!-- comment -->\n- Item";
2512 let fixed = fix(content);
2513 assert_eq!(fixed, content, "Fix should not modify already-correct content");
2514 }
2515
2516 #[test]
2517 fn test_html_comment_fix_adds_blank_when_needed() {
2518 let content = "Text.\n<!-- comment -->\n- Item";
2521 let fixed = fix(content);
2522 assert!(
2523 fixed.contains("<!-- comment -->\n\n- Item"),
2524 "Fix should add blank line before list. Got: {fixed}"
2525 );
2526 }
2527
2528 #[test]
2529 fn test_ordered_list_inside_html_comment() {
2530 let content = "<!--\n3. Starting at 3\n4. Next item\n-->";
2532 let warnings = lint(content);
2533 assert_eq!(
2534 warnings.len(),
2535 0,
2536 "Should not warn about ordered list inside HTML comment. Got: {warnings:?}"
2537 );
2538 }
2539
2540 #[test]
2547 fn test_blockquote_list_exit_no_warning() {
2548 let content = "- outer item\n > - blockquote list 1\n > - blockquote list 2\n- next outer item";
2550 let warnings = lint(content);
2551 assert_eq!(
2552 warnings.len(),
2553 0,
2554 "Should not warn when exiting blockquote. Got: {warnings:?}"
2555 );
2556 }
2557
2558 #[test]
2559 fn test_nested_blockquote_list_exit() {
2560 let content = "- outer\n - nested\n > - bq list 1\n > - bq list 2\n - back to nested\n- outer again";
2562 let warnings = lint(content);
2563 assert_eq!(
2564 warnings.len(),
2565 0,
2566 "Should not warn when exiting nested blockquote list. Got: {warnings:?}"
2567 );
2568 }
2569
2570 #[test]
2571 fn test_blockquote_same_level_no_warning() {
2572 let content = "> - item 1\n> - item 2\n> Text after";
2575 let warnings = lint(content);
2576 assert_eq!(
2577 warnings.len(),
2578 0,
2579 "Should not warn - text is lazy continuation in blockquote. Got: {warnings:?}"
2580 );
2581 }
2582
2583 #[test]
2584 fn test_blockquote_list_with_special_chars() {
2585 let content = "- Item with <>&\n > - blockquote item\n- Back to outer";
2587 let warnings = lint(content);
2588 assert_eq!(
2589 warnings.len(),
2590 0,
2591 "Special chars in content should not affect blockquote detection. Got: {warnings:?}"
2592 );
2593 }
2594
2595 #[test]
2596 fn test_lazy_continuation_whitespace_only_line() {
2597 let content = "- Item\n \nText after whitespace-only line";
2600 let config = MD032Config {
2601 allow_lazy_continuation: false,
2602 };
2603 let warnings = lint_with_config(content, config);
2604 assert_eq!(
2606 warnings.len(),
2607 0,
2608 "Whitespace-only line IS a separator in CommonMark. Got: {warnings:?}"
2609 );
2610 }
2611
2612 #[test]
2613 fn test_lazy_continuation_blockquote_context() {
2614 let content = "> - Item\n> Lazy in quote";
2616 let config = MD032Config {
2617 allow_lazy_continuation: false,
2618 };
2619 let warnings = lint_with_config(content, config);
2620 assert!(warnings.len() <= 1, "Should handle blockquote context gracefully");
2623 }
2624
2625 #[test]
2626 fn test_lazy_continuation_fix_preserves_content() {
2627 let content = "- Item with special chars: <>&\nContinuation with: \"quotes\"";
2629 let config = MD032Config {
2630 allow_lazy_continuation: false,
2631 };
2632 let fixed = fix_with_config(content, config);
2633 assert!(fixed.contains("<>&"), "Should preserve special chars");
2634 assert!(fixed.contains("\"quotes\""), "Should preserve quotes");
2635 assert_eq!(fixed, "- Item with special chars: <>&\n Continuation with: \"quotes\"");
2637 }
2638
2639 #[test]
2640 fn test_lazy_continuation_fix_idempotent() {
2641 let content = "- Item\nLazy";
2643 let config = MD032Config {
2644 allow_lazy_continuation: false,
2645 };
2646 let fixed_once = fix_with_config(content, config.clone());
2647 let fixed_twice = fix_with_config(&fixed_once, config);
2648 assert_eq!(fixed_once, fixed_twice, "Fix should be idempotent");
2649 }
2650
2651 #[test]
2652 fn test_lazy_continuation_config_default_allows() {
2653 let content = "- Item\nLazy text that continues";
2655 let default_config = MD032Config::default();
2656 assert!(
2657 default_config.allow_lazy_continuation,
2658 "Default should allow lazy continuation"
2659 );
2660 let warnings = lint_with_config(content, default_config);
2661 assert_eq!(warnings.len(), 0, "Default config should not warn on lazy continuation");
2662 }
2663
2664 #[test]
2665 fn test_lazy_continuation_after_multi_line_item() {
2666 let content = "- Item line 1\n Item line 2 (indented)\nLazy (not indented)";
2668 let config = MD032Config {
2669 allow_lazy_continuation: false,
2670 };
2671 let warnings = lint_with_config(content, config.clone());
2672 assert_eq!(
2673 warnings.len(),
2674 1,
2675 "Should warn only for the lazy line, not the indented line"
2676 );
2677 }
2678
2679 #[test]
2681 fn test_blockquote_list_with_continuation_and_nested() {
2682 let content = "> - item 1\n> continuation\n> - nested\n> - item 2";
2685 let warnings = lint(content);
2686 assert_eq!(
2687 warnings.len(),
2688 0,
2689 "Blockquoted list with continuation and nested items should have no warnings. Got: {warnings:?}"
2690 );
2691 }
2692
2693 #[test]
2694 fn test_blockquote_list_simple() {
2695 let content = "> - item 1\n> - item 2";
2697 let warnings = lint(content);
2698 assert_eq!(warnings.len(), 0, "Simple blockquoted list should have no warnings");
2699 }
2700
2701 #[test]
2702 fn test_blockquote_list_with_continuation_only() {
2703 let content = "> - item 1\n> continuation\n> - item 2";
2705 let warnings = lint(content);
2706 assert_eq!(
2707 warnings.len(),
2708 0,
2709 "Blockquoted list with continuation should have no warnings"
2710 );
2711 }
2712
2713 #[test]
2714 fn test_blockquote_list_with_lazy_continuation() {
2715 let content = "> - item 1\n> lazy continuation\n> - item 2";
2717 let warnings = lint(content);
2718 assert_eq!(
2719 warnings.len(),
2720 0,
2721 "Blockquoted list with lazy continuation should have no warnings"
2722 );
2723 }
2724
2725 #[test]
2726 fn test_nested_blockquote_list() {
2727 let content = ">> - item 1\n>> continuation\n>> - nested\n>> - item 2";
2729 let warnings = lint(content);
2730 assert_eq!(warnings.len(), 0, "Nested blockquote list should have no warnings");
2731 }
2732
2733 #[test]
2734 fn test_blockquote_list_needs_preceding_blank() {
2735 let content = "> Text before\n> - item 1\n> - item 2";
2737 let warnings = lint(content);
2738 assert_eq!(
2739 warnings.len(),
2740 1,
2741 "Should warn for missing blank before blockquoted list"
2742 );
2743 }
2744
2745 #[test]
2746 fn test_blockquote_list_properly_separated() {
2747 let content = "> Text before\n>\n> - item 1\n> - item 2\n>\n> Text after";
2749 let warnings = lint(content);
2750 assert_eq!(
2751 warnings.len(),
2752 0,
2753 "Properly separated blockquoted list should have no warnings"
2754 );
2755 }
2756
2757 #[test]
2758 fn test_blockquote_ordered_list() {
2759 let content = "> 1. item 1\n> continuation\n> 2. item 2";
2761 let warnings = lint(content);
2762 assert_eq!(warnings.len(), 0, "Ordered list in blockquote should have no warnings");
2763 }
2764
2765 #[test]
2766 fn test_blockquote_list_with_empty_blockquote_line() {
2767 let content = "> - item 1\n>\n> - item 2";
2769 let warnings = lint(content);
2770 assert_eq!(warnings.len(), 0, "Empty blockquote line should not break list");
2771 }
2772
2773 #[test]
2775 fn test_blockquote_list_multi_paragraph_items() {
2776 let content = "# Test\n\n> Some intro text\n> \n> * List item 1\n> \n> Continuation\n> * List item 2\n";
2779 let warnings = lint(content);
2780 assert_eq!(
2781 warnings.len(),
2782 0,
2783 "Multi-paragraph list items in blockquotes should have no warnings. Got: {warnings:?}"
2784 );
2785 }
2786
2787 #[test]
2789 fn test_blockquote_ordered_list_multi_paragraph_items() {
2790 let content = "> 1. First item\n> \n> Continuation of first\n> 2. Second item\n";
2791 let warnings = lint(content);
2792 assert_eq!(
2793 warnings.len(),
2794 0,
2795 "Ordered multi-paragraph list items in blockquotes should have no warnings. Got: {warnings:?}"
2796 );
2797 }
2798
2799 #[test]
2801 fn test_blockquote_list_multiple_continuations() {
2802 let content = "> - Item 1\n> \n> First continuation\n> \n> Second continuation\n> - Item 2\n";
2803 let warnings = lint(content);
2804 assert_eq!(
2805 warnings.len(),
2806 0,
2807 "Multiple continuation paragraphs should not break blockquote list. Got: {warnings:?}"
2808 );
2809 }
2810
2811 #[test]
2813 fn test_nested_blockquote_multi_paragraph_list() {
2814 let content = ">> - Item 1\n>> \n>> Continuation\n>> - Item 2\n";
2815 let warnings = lint(content);
2816 assert_eq!(
2817 warnings.len(),
2818 0,
2819 "Nested blockquote multi-paragraph list should have no warnings. Got: {warnings:?}"
2820 );
2821 }
2822
2823 #[test]
2825 fn test_triple_nested_blockquote_multi_paragraph_list() {
2826 let content = ">>> - Item 1\n>>> \n>>> Continuation\n>>> - Item 2\n";
2827 let warnings = lint(content);
2828 assert_eq!(
2829 warnings.len(),
2830 0,
2831 "Triple-nested blockquote multi-paragraph list should have no warnings. Got: {warnings:?}"
2832 );
2833 }
2834
2835 #[test]
2837 fn test_blockquote_list_last_item_continuation() {
2838 let content = "> - Item 1\n> - Item 2\n> \n> Continuation of item 2\n";
2839 let warnings = lint(content);
2840 assert_eq!(
2841 warnings.len(),
2842 0,
2843 "Last item with continuation should have no warnings. Got: {warnings:?}"
2844 );
2845 }
2846
2847 #[test]
2849 fn test_blockquote_list_first_item_only_continuation() {
2850 let content = "> - Item 1\n> \n> Continuation of item 1\n";
2851 let warnings = lint(content);
2852 assert_eq!(
2853 warnings.len(),
2854 0,
2855 "Single item with continuation should have no warnings. Got: {warnings:?}"
2856 );
2857 }
2858
2859 #[test]
2863 fn test_blockquote_level_change_breaks_list() {
2864 let content = "> - Item in single blockquote\n>> - Item in nested blockquote\n";
2866 let warnings = lint(content);
2867 assert!(
2871 warnings.len() <= 2,
2872 "Blockquote level change warnings should be reasonable. Got: {warnings:?}"
2873 );
2874 }
2875
2876 #[test]
2878 fn test_exit_blockquote_needs_blank_before_list() {
2879 let content = "> Blockquote text\n\n- List outside blockquote\n";
2881 let warnings = lint(content);
2882 assert_eq!(
2883 warnings.len(),
2884 0,
2885 "List after blank line outside blockquote should be fine. Got: {warnings:?}"
2886 );
2887
2888 let content2 = "> Blockquote text\n- List outside blockquote\n";
2892 let warnings2 = lint(content2);
2893 assert!(
2895 warnings2.len() <= 1,
2896 "List after blockquote warnings should be reasonable. Got: {warnings2:?}"
2897 );
2898 }
2899
2900 #[test]
2902 fn test_blockquote_multi_paragraph_all_unordered_markers() {
2903 let content_dash = "> - Item 1\n> \n> Continuation\n> - Item 2\n";
2905 let warnings = lint(content_dash);
2906 assert_eq!(warnings.len(), 0, "Dash marker should work. Got: {warnings:?}");
2907
2908 let content_asterisk = "> * Item 1\n> \n> Continuation\n> * Item 2\n";
2910 let warnings = lint(content_asterisk);
2911 assert_eq!(warnings.len(), 0, "Asterisk marker should work. Got: {warnings:?}");
2912
2913 let content_plus = "> + Item 1\n> \n> Continuation\n> + Item 2\n";
2915 let warnings = lint(content_plus);
2916 assert_eq!(warnings.len(), 0, "Plus marker should work. Got: {warnings:?}");
2917 }
2918
2919 #[test]
2921 fn test_blockquote_multi_paragraph_parenthesis_marker() {
2922 let content = "> 1) Item 1\n> \n> Continuation\n> 2) Item 2\n";
2923 let warnings = lint(content);
2924 assert_eq!(
2925 warnings.len(),
2926 0,
2927 "Parenthesis ordered markers should work. Got: {warnings:?}"
2928 );
2929 }
2930
2931 #[test]
2933 fn test_blockquote_multi_paragraph_multi_digit_numbers() {
2934 let content = "> 10. Item 10\n> \n> Continuation of item 10\n> 11. Item 11\n";
2936 let warnings = lint(content);
2937 assert_eq!(
2938 warnings.len(),
2939 0,
2940 "Multi-digit ordered list should work. Got: {warnings:?}"
2941 );
2942 }
2943
2944 #[test]
2946 fn test_blockquote_multi_paragraph_with_formatting() {
2947 let content = "> - Item with **bold**\n> \n> Continuation with *emphasis* and `code`\n> - Item 2\n";
2948 let warnings = lint(content);
2949 assert_eq!(
2950 warnings.len(),
2951 0,
2952 "Continuation with inline formatting should work. Got: {warnings:?}"
2953 );
2954 }
2955
2956 #[test]
2958 fn test_blockquote_multi_paragraph_all_items_have_continuation() {
2959 let content = "> - Item 1\n> \n> Continuation 1\n> - Item 2\n> \n> Continuation 2\n> - Item 3\n> \n> Continuation 3\n";
2960 let warnings = lint(content);
2961 assert_eq!(
2962 warnings.len(),
2963 0,
2964 "All items with continuations should work. Got: {warnings:?}"
2965 );
2966 }
2967
2968 #[test]
2970 fn test_blockquote_multi_paragraph_lowercase_continuation() {
2971 let content = "> - Item 1\n> \n> and this continues the item\n> - Item 2\n";
2972 let warnings = lint(content);
2973 assert_eq!(
2974 warnings.len(),
2975 0,
2976 "Lowercase continuation should work. Got: {warnings:?}"
2977 );
2978 }
2979
2980 #[test]
2982 fn test_blockquote_multi_paragraph_uppercase_continuation() {
2983 let content = "> - Item 1\n> \n> This continues the item with uppercase\n> - Item 2\n";
2984 let warnings = lint(content);
2985 assert_eq!(
2986 warnings.len(),
2987 0,
2988 "Uppercase continuation with proper indent should work. Got: {warnings:?}"
2989 );
2990 }
2991
2992 #[test]
2994 fn test_blockquote_separate_ordered_unordered_multi_paragraph() {
2995 let content = "> - Unordered item\n> \n> Continuation\n> \n> 1. Ordered item\n> \n> Continuation\n";
2997 let warnings = lint(content);
2998 assert!(
3000 warnings.len() <= 1,
3001 "Separate lists with continuations should be reasonable. Got: {warnings:?}"
3002 );
3003 }
3004
3005 #[test]
3007 fn test_blockquote_multi_paragraph_bare_marker_blank() {
3008 let content = "> - Item 1\n>\n> Continuation\n> - Item 2\n";
3010 let warnings = lint(content);
3011 assert_eq!(warnings.len(), 0, "Bare > as blank line should work. Got: {warnings:?}");
3012 }
3013
3014 #[test]
3015 fn test_blockquote_list_varying_spaces_after_marker() {
3016 let content = "> - item 1\n> continuation with more indent\n> - item 2";
3018 let warnings = lint(content);
3019 assert_eq!(warnings.len(), 0, "Varying spaces after > should not break list");
3020 }
3021
3022 #[test]
3023 fn test_deeply_nested_blockquote_list() {
3024 let content = ">>> - item 1\n>>> continuation\n>>> - item 2";
3026 let warnings = lint(content);
3027 assert_eq!(
3028 warnings.len(),
3029 0,
3030 "Deeply nested blockquote list should have no warnings"
3031 );
3032 }
3033
3034 #[test]
3035 fn test_blockquote_level_change_in_list() {
3036 let content = "> - item 1\n>> - deeper item\n> - item 2";
3038 let warnings = lint(content);
3041 assert!(
3042 !warnings.is_empty(),
3043 "Blockquote level change should break list and trigger warnings"
3044 );
3045 }
3046
3047 #[test]
3048 fn test_blockquote_list_with_code_span() {
3049 let content = "> - item with `code`\n> continuation\n> - item 2";
3051 let warnings = lint(content);
3052 assert_eq!(
3053 warnings.len(),
3054 0,
3055 "Blockquote list with code span should have no warnings"
3056 );
3057 }
3058
3059 #[test]
3060 fn test_code_span_html_comment_delimiters_no_false_positive() {
3061 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";
3067 let warnings = lint(content);
3068 assert_eq!(
3069 warnings.len(),
3070 0,
3071 "code-span HTML comment delimiters must not cause MD032 false positives, got: {warnings:?}"
3072 );
3073 }
3074
3075 #[test]
3076 fn test_code_span_html_comment_delimiters_fix_is_idempotent() {
3077 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";
3082 let fixed = fix(content);
3083 assert_eq!(
3084 fixed, content,
3085 "MD032 fix must be a no-op for content whose only `<!--`/`-->` are inside code spans"
3086 );
3087 }
3088
3089 #[test]
3090 fn test_blockquote_list_at_document_end() {
3091 let content = "> Some text\n>\n> - item 1\n> - item 2";
3093 let warnings = lint(content);
3094 assert_eq!(
3095 warnings.len(),
3096 0,
3097 "Blockquote list at document end should have no warnings"
3098 );
3099 }
3100
3101 #[test]
3102 fn test_fix_preserves_blockquote_prefix_before_list() {
3103 let content = "> Text before
3105> - Item 1
3106> - Item 2";
3107 let fixed = fix(content);
3108
3109 let expected = "> Text before
3111>
3112> - Item 1
3113> - Item 2";
3114 assert_eq!(
3115 fixed, expected,
3116 "Fix should insert '>' blank line, not plain blank line"
3117 );
3118 }
3119
3120 #[test]
3121 fn test_fix_preserves_triple_nested_blockquote_prefix_for_list() {
3122 let content = ">>> Triple nested
3125>>> - Item 1
3126>>> - Item 2
3127>>> More text";
3128 let fixed = fix(content);
3129
3130 let expected = ">>> Triple nested
3132>>>
3133>>> - Item 1
3134>>> - Item 2
3135>>> More text";
3136 assert_eq!(
3137 fixed, expected,
3138 "Fix should preserve triple-nested blockquote prefix '>>>'"
3139 );
3140 }
3141
3142 fn lint_quarto(content: &str) -> Vec<LintWarning> {
3145 let rule = MD032BlanksAroundLists::default();
3146 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
3147 rule.check(&ctx).unwrap()
3148 }
3149
3150 #[test]
3151 fn test_quarto_list_after_div_open() {
3152 let content = "Content\n\n::: {.callout-note}\n- Item 1\n- Item 2\n:::\n";
3154 let warnings = lint_quarto(content);
3155 assert!(
3157 warnings.is_empty(),
3158 "Quarto div marker should be transparent before list: {warnings:?}"
3159 );
3160 }
3161
3162 #[test]
3163 fn test_quarto_list_before_div_close() {
3164 let content = "::: {.callout-note}\n\n- Item 1\n- Item 2\n:::\n";
3166 let warnings = lint_quarto(content);
3167 assert!(
3169 warnings.is_empty(),
3170 "Quarto div marker should be transparent after list: {warnings:?}"
3171 );
3172 }
3173
3174 #[test]
3175 fn test_quarto_list_needs_blank_without_div() {
3176 let content = "Content\n::: {.callout-note}\n- Item 1\n- Item 2\n:::\n";
3178 let warnings = lint_quarto(content);
3179 assert!(
3182 !warnings.is_empty(),
3183 "Should still require blank when not present: {warnings:?}"
3184 );
3185 }
3186
3187 #[test]
3188 fn test_quarto_list_in_callout_with_content() {
3189 let content = "::: {.callout-note}\nNote introduction:\n\n- Item 1\n- Item 2\n\nMore note content.\n:::\n";
3191 let warnings = lint_quarto(content);
3192 assert!(
3193 warnings.is_empty(),
3194 "List with proper blanks inside callout should pass: {warnings:?}"
3195 );
3196 }
3197
3198 #[test]
3199 fn test_quarto_div_markers_not_transparent_in_standard_flavor() {
3200 let content = "Content\n\n:::\n- Item 1\n- Item 2\n:::\n";
3202 let warnings = lint(content); assert!(
3205 !warnings.is_empty(),
3206 "Standard flavor should not treat ::: as transparent: {warnings:?}"
3207 );
3208 }
3209
3210 #[test]
3211 fn test_quarto_nested_divs_with_list() {
3212 let content = "::: {.outer}\n::: {.inner}\n\n- Item 1\n- Item 2\n\n:::\n:::\n";
3214 let warnings = lint_quarto(content);
3215 assert!(warnings.is_empty(), "Nested divs with list should work: {warnings:?}");
3216 }
3217
3218 #[test]
3219 fn test_issue512_complex_nested_list_with_continuation() {
3220 let content = "\
3223- First level of indentation.
3224 - Second level of indentation.
3225 - Third level of indentation.
3226 - Third level of indentation.
3227
3228 Second level list continuation.
3229
3230 First level list continuation.
3231- First level of indentation.
3232";
3233 let warnings = lint(content);
3234 assert!(
3235 warnings.is_empty(),
3236 "Nested list with parent-level continuation should produce no warnings. Got: {warnings:?}"
3237 );
3238 }
3239
3240 #[test]
3241 fn test_issue512_continuation_at_root_level() {
3242 let content = "\
3246- First level.
3247 - Second level.
3248
3249 First level continuation.
3250
3251Root level lazy continuation.
3252- Another first level item.
3253";
3254 let warnings = lint(content);
3255 assert_eq!(
3256 warnings.len(),
3257 1,
3258 "Should warn on line 7 (new list after break). Got: {warnings:?}"
3259 );
3260 assert_eq!(warnings[0].line, 7);
3261 }
3262
3263 #[test]
3264 fn test_issue512_three_level_nesting_continuation_at_each_level() {
3265 let content = "\
3267- Level 1 item.
3268 - Level 2 item.
3269 - Level 3 item.
3270
3271 Level 3 continuation.
3272
3273 Level 2 continuation.
3274
3275 Level 1 continuation (indented under marker).
3276- Another level 1 item.
3277";
3278 let warnings = lint(content);
3279 assert!(
3280 warnings.is_empty(),
3281 "Continuation at each nesting level should produce no warnings. Got: {warnings:?}"
3282 );
3283 }
3284
3285 #[test]
3286 fn test_pandoc_list_after_div_open() {
3287 let rule = MD032BlanksAroundLists::default();
3290 let content = "Content\n\n::: {.callout-note}\n- Item 1\n- Item 2\n:::\n";
3291 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
3292 let warnings = rule.check(&ctx).unwrap();
3293 assert!(
3294 warnings.is_empty(),
3295 "MD032 should treat Pandoc div marker as transparent before list: {warnings:?}"
3296 );
3297 }
3298
3299 #[test]
3300 fn test_md032_html_comment() {
3301 let rule = MD032BlanksAroundLists::default();
3302 let content = "text\n<!--\n- Item 1\n- Item 2\n-->\ntext";
3303 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3304 let warnings = rule.check(&ctx).unwrap();
3305 assert!(
3306 warnings.is_empty(),
3307 "MD032 should not require blank lines around lists inside HTML comments: {warnings:?}"
3308 );
3309 }
3310
3311 #[test]
3312 fn test_mkdocs_admonition_nested_ordered_list_not_flagged() {
3313 let rule = MD032BlanksAroundLists::default();
3319 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";
3320 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3321 let warnings = rule.check(&ctx).unwrap();
3322 assert!(
3323 warnings.is_empty(),
3324 "admonition-nested ordered list should not be flagged: {warnings:?}"
3325 );
3326 }
3327
3328 #[test]
3329 fn test_mkdocs_admonition_nested_ordered_list_cascade_not_flagged() {
3330 let rule = MD032BlanksAroundLists::default();
3334 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";
3335 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3336 let warnings = rule.check(&ctx).unwrap();
3337 assert!(
3338 warnings.is_empty(),
3339 "cascading admonition-nested ordered list should not be flagged: {warnings:?}"
3340 );
3341 }
3342
3343 #[test]
3344 fn test_mkdocs_content_tab_nested_ordered_list_not_flagged() {
3345 let rule = MD032BlanksAroundLists::default();
3347 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";
3348 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3349 let warnings = rule.check(&ctx).unwrap();
3350 assert!(
3351 warnings.is_empty(),
3352 "content-tab-nested ordered list should not be flagged: {warnings:?}"
3353 );
3354 }
3355
3356 #[test]
3357 fn test_mkdocs_admonition_prose_then_non1_item_still_flagged() {
3358 let rule = MD032BlanksAroundLists::default();
3364 let content = "1. no error here\n\n!!! example\n\n Intro.\n 2. item\n";
3365 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3366 let warnings = rule.check(&ctx).unwrap();
3367 assert_eq!(
3368 warnings.len(),
3369 1,
3370 "prose then non-1 item inside an admonition must stay flagged: {warnings:?}"
3371 );
3372 }
3373
3374 #[test]
3375 fn test_mkdocs_admonition_prose_after_list_item_breaks_continuation() {
3376 let rule = MD032BlanksAroundLists::default();
3381 let content = "1. no error here\n\n!!! example\n\n 1. one.\n Intro prose.\n 2. two\n";
3382 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3383 let warnings = rule.check(&ctx).unwrap();
3384 assert_eq!(
3385 warnings.len(),
3386 1,
3387 "prose at item indent breaks the list continuation, item must stay flagged: {warnings:?}"
3388 );
3389 }
3390
3391 #[test]
3392 fn test_mkdocs_admonition_wrapped_item_continuation_not_flagged() {
3393 let rule = MD032BlanksAroundLists::default();
3398 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";
3399 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3400 let warnings = rule.check(&ctx).unwrap();
3401 assert!(
3402 warnings.is_empty(),
3403 "wrapped continuation of a nested list item must not be flagged: {warnings:?}"
3404 );
3405 }
3406
3407 #[test]
3408 fn test_mkdocs_ambiguous_prose_non1_ordered_item_still_flagged() {
3409 let rule = MD032BlanksAroundLists::default();
3415 let content = "1. no error here\n\nno error here.\n2. error here because previous line ends with a period.\n";
3416 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3417 let warnings = rule.check(&ctx).unwrap();
3418 assert_eq!(
3419 warnings.len(),
3420 1,
3421 "ambiguous non-1 ordered item outside any container should still be flagged: {warnings:?}"
3422 );
3423 assert_eq!(warnings[0].line, 4);
3424 assert!(warnings[0].message.contains("non-1"));
3425 }
3426
3427 #[test]
3428 fn test_mkdocs_admonition_nested_list_without_trailing_punctuation_not_flagged() {
3429 let rule = MD032BlanksAroundLists::default();
3435 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";
3436 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3437 let warnings = rule.check(&ctx).unwrap();
3438 assert!(
3439 warnings.is_empty(),
3440 "admonition-nested ordered list without trailing punctuation should not be flagged: {warnings:?}"
3441 );
3442 }
3443
3444 #[test]
3445 fn test_standard_flavor_admonition_indented_list_unchanged() {
3446 let rule = MD032BlanksAroundLists::default();
3451 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";
3452 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3453 let warnings = rule.check(&ctx).unwrap();
3454 assert!(
3455 warnings.is_empty(),
3456 "indented code block under standard flavor should not be flagged: {warnings:?}"
3457 );
3458 }
3459
3460 #[test]
3461 fn test_mkdocs_html_markdown_div_nested_ordered_list_still_flagged() {
3462 let rule = MD032BlanksAroundLists::default();
3467 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";
3468 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3469 let warnings = rule.check(&ctx).unwrap();
3470 assert_eq!(
3471 warnings.len(),
3472 1,
3473 "markdown=\"1\" div nested ordered list behavior must stay unchanged: {warnings:?}"
3474 );
3475 assert_eq!(warnings[0].line, 6);
3476 }
3477
3478 #[test]
3479 fn test_pseudo_list_marker_after_list() {
3480 let content = indoc::indoc! {"
3481 - Item 1
3482 Item 1 content.
3483
3484 The unsigned-integer types may be written `uN`, with `N` a positive multiple of
3485 8. Unsigned integer types wrap around on overflow; we strongly advise that they
3486 are not used except when those semantics are desired.
3487 "};
3488 let warnings = lint(content);
3489 assert!(
3490 warnings.is_empty(),
3491 "Expected no warnings for pseudo-list marker after list, but got: {warnings:?}"
3492 );
3493 }
3494
3495 #[test]
3496 fn test_pseudo_list_marker_without_preceding_list() {
3497 let content = indoc::indoc! {"
3498 The unsigned-integer types may be written `uN`, with `N` a positive multiple of
3499 8. Unsigned integer types wrap around on overflow; we strongly advise that they
3500 are not used except when those semantics are desired.
3501 "};
3502 let warnings = lint(content);
3503 assert!(
3504 warnings.is_empty(),
3505 "Expected no warnings for pseudo-list marker without preceding list, but got: {warnings:?}"
3506 );
3507 }
3508}