1use crate::lint_context::LazyContLine;
2use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
3use crate::utils::blockquote::{content_after_blockquote, effective_indent_in_blockquote};
4use crate::utils::calculate_indentation_width_default;
5use crate::utils::pandoc;
6use crate::utils::range_utils::{LineIndex, calculate_line_range};
7use crate::utils::regex_cache::BLOCKQUOTE_PREFIX_RE;
8use regex::Regex;
9use std::sync::LazyLock;
10
11mod md032_config;
12pub(super) use md032_config::MD032Config;
13
14static ORDERED_LIST_NON_ONE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*([2-9]|\d{2,})\.\s").unwrap());
16
17fn is_thematic_break(line: &str) -> bool {
20 if calculate_indentation_width_default(line) > 3 {
22 return false;
23 }
24
25 let trimmed = line.trim();
26 if trimmed.len() < 3 {
27 return false;
28 }
29
30 let chars: Vec<char> = trimmed.chars().collect();
31 let first_non_space = chars.iter().find(|&&c| c != ' ');
32
33 if let Some(&marker) = first_non_space {
34 if marker != '-' && marker != '*' && marker != '_' {
35 return false;
36 }
37 let marker_count = chars.iter().filter(|&&c| c == marker).count();
38 let other_count = chars.iter().filter(|&&c| c != marker && c != ' ').count();
39 marker_count >= 3 && other_count == 0
40 } else {
41 false
42 }
43}
44
45#[derive(Debug, Clone, Default)]
115pub struct MD032BlanksAroundLists {
116 config: MD032Config,
117}
118
119impl MD032BlanksAroundLists {
120 pub fn from_config_struct(config: MD032Config) -> Self {
121 Self { config }
122 }
123}
124
125impl MD032BlanksAroundLists {
126 fn should_require_blank_line_before(
128 ctx: &crate::lint_context::LintContext,
129 prev_line_num: usize,
130 current_line_num: usize,
131 ) -> bool {
132 if ctx
134 .line_info(prev_line_num)
135 .is_some_and(|info| info.in_code_block || info.in_front_matter)
136 {
137 return true;
138 }
139
140 if Self::is_nested_list(ctx, prev_line_num, current_line_num) {
142 return false;
143 }
144
145 true
147 }
148
149 fn is_nested_list(
151 ctx: &crate::lint_context::LintContext,
152 prev_line_num: usize, current_line_num: usize, ) -> bool {
155 if current_line_num > 0 && current_line_num - 1 < ctx.lines.len() {
157 let current_line = &ctx.lines[current_line_num - 1];
158 if current_line.indent >= 2 {
159 if prev_line_num > 0 && prev_line_num - 1 < ctx.lines.len() {
161 let prev_line = &ctx.lines[prev_line_num - 1];
162 if prev_line.list_item.is_some() || prev_line.indent >= 2 {
164 return true;
165 }
166 }
167 }
168 }
169 false
170 }
171
172 fn should_apply_lazy_fix(ctx: &crate::lint_context::LintContext, line_num: usize) -> bool {
175 ctx.lines
176 .get(line_num.saturating_sub(1))
177 .is_some_and(|li| !li.in_code_block && !li.in_front_matter && !li.in_html_comment && !li.in_mdx_comment)
178 }
179
180 fn calculate_lazy_continuation_fix(
183 ctx: &crate::lint_context::LintContext,
184 line_num: usize,
185 lazy_info: &LazyContLine,
186 ) -> Option<Fix> {
187 let line_info = ctx.lines.get(line_num.saturating_sub(1))?;
188 let line_content = line_info.content(ctx.content);
189
190 if lazy_info.blockquote_level == 0 {
191 let start_byte = line_info.byte_offset;
193 let end_byte = start_byte + lazy_info.current_indent;
194 let replacement = " ".repeat(lazy_info.expected_indent);
195
196 Some(Fix::new(start_byte..end_byte, replacement))
197 } else {
198 let after_bq = content_after_blockquote(line_content, lazy_info.blockquote_level);
200 let prefix_byte_len = line_content.len().saturating_sub(after_bq.len());
201 if prefix_byte_len == 0 {
202 return None;
203 }
204
205 let current_indent = after_bq.len() - after_bq.trim_start().len();
206 let start_byte = line_info.byte_offset + prefix_byte_len;
207 let end_byte = start_byte + current_indent;
208 let replacement = " ".repeat(lazy_info.expected_indent);
209
210 Some(Fix::new(start_byte..end_byte, replacement))
211 }
212 }
213
214 fn apply_lazy_fix_to_line(line: &str, lazy_info: &LazyContLine) -> String {
217 if lazy_info.blockquote_level == 0 {
218 let content = line.trim_start();
220 format!("{}{}", " ".repeat(lazy_info.expected_indent), content)
221 } else {
222 let after_bq = content_after_blockquote(line, lazy_info.blockquote_level);
224 let prefix_len = line.len().saturating_sub(after_bq.len());
225 if prefix_len == 0 {
226 return line.to_string();
227 }
228
229 let prefix = &line[..prefix_len];
230 let rest = after_bq.trim_start();
231 format!("{}{}{}", prefix, " ".repeat(lazy_info.expected_indent), rest)
232 }
233 }
234
235 fn find_preceding_content(ctx: &crate::lint_context::LintContext, before_line: usize) -> (usize, bool) {
243 let is_pandoc = ctx.flavor.is_pandoc_compatible();
244 for line_num in (1..before_line).rev() {
245 let idx = line_num - 1;
246 if let Some(info) = ctx.lines.get(idx) {
247 if info.in_html_comment || info.in_mdx_comment {
249 continue;
250 }
251 if is_pandoc {
253 let trimmed = info.content(ctx.content).trim();
254 if pandoc::is_div_open(trimmed) || pandoc::is_div_close(trimmed) {
255 continue;
256 }
257 }
258 return (line_num, info.is_blank);
259 }
260 }
261 (0, true)
263 }
264
265 fn find_following_content(ctx: &crate::lint_context::LintContext, after_line: usize) -> (usize, bool) {
272 let is_pandoc = ctx.flavor.is_pandoc_compatible();
273 let num_lines = ctx.lines.len();
274 for line_num in (after_line + 1)..=num_lines {
275 let idx = line_num - 1;
276 if let Some(info) = ctx.lines.get(idx) {
277 if info.in_html_comment || info.in_mdx_comment {
279 continue;
280 }
281 if is_pandoc {
283 let trimmed = info.content(ctx.content).trim();
284 if pandoc::is_div_open(trimmed) || pandoc::is_div_close(trimmed) {
285 continue;
286 }
287 }
288 return (line_num, info.is_blank);
289 }
290 }
291 (0, true)
293 }
294
295 fn convert_list_blocks(&self, ctx: &crate::lint_context::LintContext) -> Vec<(usize, usize, String)> {
297 let mut blocks: Vec<(usize, usize, String)> = Vec::new();
298
299 for block in &ctx.list_blocks {
300 if ctx
302 .line_info(block.start_line)
303 .is_some_and(|info| info.in_footnote_definition)
304 {
305 continue;
306 }
307
308 let mut segments: Vec<(usize, usize)> = Vec::new();
314 let mut current_start = block.start_line;
315 let mut prev_item_line = 0;
316
317 let get_blockquote_level = |line_num: usize| -> usize {
319 if line_num == 0 || line_num > ctx.lines.len() {
320 return 0;
321 }
322 let line_content = ctx.lines[line_num - 1].content(ctx.content);
323 BLOCKQUOTE_PREFIX_RE
324 .find(line_content)
325 .map_or(0, |m| m.as_str().chars().filter(|&c| c == '>').count())
326 };
327
328 let mut prev_bq_level = 0;
329
330 for &item_line in &block.item_lines {
331 let current_bq_level = get_blockquote_level(item_line);
332
333 if prev_item_line > 0 {
334 let blockquote_level_changed = prev_bq_level != current_bq_level;
336
337 let mut has_standalone_code_fence = false;
340
341 let min_indent_for_content = if block.is_ordered {
343 3 } else {
347 2 };
350
351 for check_line in (prev_item_line + 1)..item_line {
352 if check_line - 1 < ctx.lines.len() {
353 let line = &ctx.lines[check_line - 1];
354 let line_content = line.content(ctx.content);
355 if line.in_code_block
356 && (line_content.trim().starts_with("```") || line_content.trim().starts_with("~~~"))
357 {
358 if line.indent < min_indent_for_content {
361 has_standalone_code_fence = true;
362 break;
363 }
364 }
365 }
366 }
367
368 if has_standalone_code_fence || blockquote_level_changed {
369 segments.push((current_start, prev_item_line));
371 current_start = item_line;
372 }
373 }
374 prev_item_line = item_line;
375 prev_bq_level = current_bq_level;
376 }
377
378 if prev_item_line > 0 {
381 segments.push((current_start, prev_item_line));
382 }
383
384 let has_code_fence_splits = segments.len() > 1 && {
386 let mut found_fence = false;
388 for i in 0..segments.len() - 1 {
389 let seg_end = segments[i].1;
390 let next_start = segments[i + 1].0;
391 for check_line in (seg_end + 1)..next_start {
393 if check_line - 1 < ctx.lines.len() {
394 let line = &ctx.lines[check_line - 1];
395 let line_content = line.content(ctx.content);
396 if line.in_code_block
397 && (line_content.trim().starts_with("```") || line_content.trim().starts_with("~~~"))
398 {
399 found_fence = true;
400 break;
401 }
402 }
403 }
404 if found_fence {
405 break;
406 }
407 }
408 found_fence
409 };
410
411 for (start, end) in &segments {
413 let mut actual_end = *end;
415
416 if !has_code_fence_splits && *end < block.end_line {
419 let block_bq_level = block.blockquote_prefix.chars().filter(|&c| c == '>').count();
421
422 let min_continuation_indent = if block_bq_level > 0 {
425 if block.is_ordered {
427 block.max_marker_width
428 } else {
429 2 }
431 } else {
432 ctx.lines
433 .get(*end - 1)
434 .and_then(|line_info| line_info.list_item.as_ref())
435 .map_or(2, |item| item.content_column)
436 };
437
438 for check_line in (*end + 1)..=block.end_line {
439 if check_line - 1 < ctx.lines.len() {
440 let line = &ctx.lines[check_line - 1];
441 let line_content = line.content(ctx.content);
442 if block.item_lines.contains(&check_line) || line.heading.is_some() {
444 break;
445 }
446 if line.in_code_block {
448 break;
449 }
450
451 let effective_indent =
453 effective_indent_in_blockquote(line_content, block_bq_level, line.indent);
454
455 if effective_indent >= min_continuation_indent {
457 actual_end = check_line;
458 }
459 else if !line.is_blank
464 && line.heading.is_none()
465 && !block.item_lines.contains(&check_line)
466 && !is_thematic_break(line_content)
467 {
468 actual_end = check_line;
470 } else if !line.is_blank {
471 break;
473 }
474 }
475 }
476 }
477
478 blocks.push((*start, actual_end, block.blockquote_prefix.clone()));
479 }
480 }
481
482 blocks.retain(|(start, end, _)| {
484 let all_in_comment = (*start..=*end).all(|line_num| {
486 ctx.lines
487 .get(line_num - 1)
488 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
489 });
490 !all_in_comment
491 });
492
493 blocks
494 }
495
496 fn perform_checks(
497 &self,
498 ctx: &crate::lint_context::LintContext,
499 lines: &[&str],
500 list_blocks: &[(usize, usize, String)],
501 line_index: &LineIndex,
502 ) -> Vec<LintWarning> {
503 let mut warnings = Vec::new();
504 let num_lines = lines.len();
505
506 for (line_idx, line) in lines.iter().enumerate() {
509 let line_num = line_idx + 1;
510
511 let is_in_list = list_blocks
513 .iter()
514 .any(|(start, end, _)| line_num >= *start && line_num <= *end);
515 if is_in_list {
516 continue;
517 }
518
519 if ctx.line_info(line_num).is_some_and(|info| {
521 info.in_code_block
522 || info.in_front_matter
523 || info.in_html_comment
524 || info.in_mdx_comment
525 || info.in_html_block
526 || info.in_jsx_block
527 }) {
528 continue;
529 }
530
531 if ORDERED_LIST_NON_ONE_RE.is_match(line) {
533 if line_idx > 0 {
535 let prev_line = lines[line_idx - 1];
536 let prev_is_blank = is_blank_in_context(prev_line);
537 let prev_line_info = ctx.line_info(line_idx);
538 let prev_excluded = prev_line_info.is_some_and(|info| info.in_code_block || info.in_front_matter);
539
540 let prev_in_mkdocs_container =
556 prev_line_info.is_some_and(|info| info.in_admonition || info.in_content_tab);
557 let continues_stale_container_list = prev_in_mkdocs_container && {
558 let item_indent = calculate_indentation_width_default(line);
559 let mut found_marker = false;
560 for j in (0..line_idx).rev() {
561 let in_container = ctx
562 .line_info(j + 1)
563 .is_some_and(|info| info.in_admonition || info.in_content_tab);
564 if !in_container {
565 break;
566 }
567 let candidate = lines[j];
568 if is_blank_in_context(candidate) {
569 continue;
570 }
571 let candidate_indent = calculate_indentation_width_default(candidate);
572 if crate::utils::regex_cache::ORDERED_LIST_MARKER_REGEX.is_match(candidate)
573 && candidate_indent == item_indent
574 {
575 found_marker = true;
576 break;
577 }
578 if candidate_indent <= item_indent {
579 break;
580 }
581 }
582 found_marker
583 };
584
585 let prev_trimmed = prev_line.trim();
590 let is_sentence_continuation = continues_stale_container_list
591 || (!prev_is_blank
592 && !prev_trimmed.is_empty()
593 && !prev_trimmed.ends_with('.')
594 && !prev_trimmed.ends_with('!')
595 && !prev_trimmed.ends_with('?')
596 && !prev_trimmed.ends_with(':')
597 && !prev_trimmed.ends_with(';')
598 && !prev_trimmed.ends_with('>')
599 && !prev_trimmed.ends_with('-')
600 && !prev_trimmed.ends_with('*'));
601
602 if prev_is_blank || !is_sentence_continuation {
603 if !prev_is_blank && !prev_excluded {
604 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_num, line);
606
607 let bq_prefix = ctx.blockquote_prefix_for_blank_line(line_idx);
608 warnings.push(LintWarning {
609 line: start_line,
610 column: start_col,
611 end_line,
612 end_column: end_col,
613 severity: Severity::Warning,
614 rule_name: Some(self.name().to_string()),
615 message: "Ordered list starting with non-1 should be preceded by blank line"
616 .to_string(),
617 fix: Some(Fix::new(
618 line_index.line_col_to_byte_range_with_length(line_num, 1, 0),
619 format!("{bq_prefix}\n"),
620 )),
621 });
622 }
623
624 if line_idx + 1 < num_lines {
627 let next_line = lines[line_idx + 1];
628 let next_is_blank = is_blank_in_context(next_line);
629 let next_excluded = ctx.line_info(line_idx + 2).is_some_and(|info| info.in_front_matter);
630
631 if !next_is_blank && !next_excluded && !next_line.trim().is_empty() {
632 let next_trimmed = next_line.trim_start();
636 let next_is_ordered_content = ORDERED_LIST_NON_ONE_RE.is_match(next_line)
637 || next_line.starts_with("1. ")
638 || (next_line.len() > next_trimmed.len()
639 && !next_trimmed.starts_with("- ")
640 && !next_trimmed.starts_with("* ")
641 && !next_trimmed.starts_with("+ ")); if !next_is_ordered_content {
644 let (start_line, start_col, end_line, end_col) =
645 calculate_line_range(line_num, line);
646 let bq_prefix = ctx.blockquote_prefix_for_blank_line(line_idx);
647 warnings.push(LintWarning {
648 line: start_line,
649 column: start_col,
650 end_line,
651 end_column: end_col,
652 severity: Severity::Warning,
653 rule_name: Some(self.name().to_string()),
654 message: "List should be followed by blank line".to_string(),
655 fix: Some(Fix::new(
656 line_index.line_col_to_byte_range_with_length(line_num + 1, 1, 0),
657 format!("{bq_prefix}\n"),
658 )),
659 });
660 }
661 }
662 }
663 }
664 }
665 }
666 }
667
668 for &(start_line, end_line, ref prefix) in list_blocks {
669 if ctx
671 .line_info(start_line)
672 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
673 {
674 continue;
675 }
676
677 if start_line > 1 {
678 let (content_line, has_blank_separation) = Self::find_preceding_content(ctx, start_line);
680
681 if !has_blank_separation && content_line > 0 {
683 let prev_line_str = lines[content_line - 1];
684 let is_prev_excluded = ctx
685 .line_info(content_line)
686 .is_some_and(|info| info.in_code_block || info.in_front_matter);
687 let prev_prefix = BLOCKQUOTE_PREFIX_RE.find(prev_line_str).map_or("", |m| m.as_str());
688 let prefixes_match = prev_prefix.trim() == prefix.trim();
689
690 let should_require = Self::should_require_blank_line_before(ctx, content_line, start_line);
693 if !is_prev_excluded && prefixes_match && should_require {
694 let (start_line, start_col, end_line, end_col) =
696 calculate_line_range(start_line, lines[start_line - 1]);
697
698 warnings.push(LintWarning {
699 line: start_line,
700 column: start_col,
701 end_line,
702 end_column: end_col,
703 severity: Severity::Warning,
704 rule_name: Some(self.name().to_string()),
705 message: "List should be preceded by blank line".to_string(),
706 fix: Some(Fix::new(
707 line_index.line_col_to_byte_range_with_length(start_line, 1, 0),
708 format!("{prefix}\n"),
709 )),
710 });
711 }
712 }
713 }
714
715 if end_line < num_lines {
716 let (content_line, has_blank_separation) = Self::find_following_content(ctx, end_line);
718
719 if !has_blank_separation && content_line > 0 {
721 let next_line_str = lines[content_line - 1];
722 let is_next_excluded = ctx.line_info(content_line).is_some_and(|info| info.in_front_matter)
725 || (content_line <= ctx.lines.len()
726 && ctx.lines[content_line - 1].in_code_block
727 && ctx.lines[content_line - 1].indent >= 2);
728 let next_prefix = BLOCKQUOTE_PREFIX_RE.find(next_line_str).map_or("", |m| m.as_str());
729
730 let end_line_str = lines[end_line - 1];
735 let end_line_prefix = BLOCKQUOTE_PREFIX_RE.find(end_line_str).map_or("", |m| m.as_str());
736 let end_line_bq_level = end_line_prefix.chars().filter(|&c| c == '>').count();
737 let next_line_bq_level = next_prefix.chars().filter(|&c| c == '>').count();
738 let exits_blockquote = end_line_bq_level > 0 && next_line_bq_level < end_line_bq_level;
739
740 let prefixes_match = next_prefix.trim() == prefix.trim();
741
742 let is_tight_continuation_of_last_item = ctx
748 .lines
749 .get(end_line - 1)
750 .and_then(|last_li| last_li.list_item.as_ref())
751 .is_some_and(|last_item| {
752 let marker_col = last_item.marker_column;
753 ctx.lines.get(content_line - 1).is_some_and(|next_li| {
754 !next_li.is_blank && next_li.list_item.is_none() && next_li.indent > marker_col
755 })
756 });
757
758 if !is_next_excluded && prefixes_match && !exits_blockquote && !is_tight_continuation_of_last_item {
761 let (start_line_last, start_col_last, end_line_last, end_col_last) =
763 calculate_line_range(end_line, lines[end_line - 1]);
764
765 warnings.push(LintWarning {
766 line: start_line_last,
767 column: start_col_last,
768 end_line: end_line_last,
769 end_column: end_col_last,
770 severity: Severity::Warning,
771 rule_name: Some(self.name().to_string()),
772 message: "List should be followed by blank line".to_string(),
773 fix: Some(Fix::new(
774 line_index.line_col_to_byte_range_with_length(end_line + 1, 1, 0),
775 format!("{prefix}\n"),
776 )),
777 });
778 }
779 }
780 }
781 }
782 warnings
783 }
784}
785
786impl Rule for MD032BlanksAroundLists {
787 fn name(&self) -> &'static str {
788 "MD032"
789 }
790
791 fn description(&self) -> &'static str {
792 "Lists should be surrounded by blank lines"
793 }
794
795 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
796 let lines = ctx.raw_lines();
797 let line_index = &ctx.line_index;
798
799 if lines.is_empty() {
801 return Ok(Vec::new());
802 }
803
804 let list_blocks = self.convert_list_blocks(ctx);
805
806 if list_blocks.is_empty() {
807 return Ok(Vec::new());
808 }
809
810 let mut warnings = self.perform_checks(ctx, lines, &list_blocks, line_index);
811
812 if !self.config.allow_lazy_continuation {
817 let lazy_cont_lines = ctx.lazy_continuation_lines();
818
819 for lazy_info in lazy_cont_lines.iter() {
820 let line_num = lazy_info.line_num;
821
822 let is_within_block = list_blocks
826 .iter()
827 .any(|(start, end, _)| line_num >= *start && line_num <= *end);
828
829 if !is_within_block {
830 continue;
831 }
832
833 let line_content = lines.get(line_num.saturating_sub(1)).unwrap_or(&"");
835 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_num, line_content);
836
837 let fix = if Self::should_apply_lazy_fix(ctx, line_num) {
839 Self::calculate_lazy_continuation_fix(ctx, line_num, lazy_info)
840 } else {
841 None
842 };
843
844 warnings.push(LintWarning {
845 line: start_line,
846 column: start_col,
847 end_line,
848 end_column: end_col,
849 severity: Severity::Warning,
850 rule_name: Some(self.name().to_string()),
851 message: "Lazy continuation line should be properly indented or preceded by blank line".to_string(),
852 fix,
853 });
854 }
855 }
856
857 Ok(warnings)
858 }
859
860 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
861 Ok(self.fix_with_structure_impl(ctx))
862 }
863
864 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
865 ctx.content.is_empty() || ctx.list_blocks.is_empty()
868 }
869
870 fn category(&self) -> RuleCategory {
871 RuleCategory::List
872 }
873
874 fn as_any(&self) -> &dyn std::any::Any {
875 self
876 }
877
878 crate::impl_rule_config_methods!(MD032Config);
879}
880
881impl MD032BlanksAroundLists {
882 fn fix_with_structure_impl(&self, ctx: &crate::lint_context::LintContext) -> String {
884 let lines = ctx.raw_lines();
885 let num_lines = lines.len();
886 if num_lines == 0 {
887 return String::new();
888 }
889
890 let list_blocks = self.convert_list_blocks(ctx);
891 if list_blocks.is_empty() {
892 return ctx.content.to_string();
893 }
894
895 let mut lazy_fixes: std::collections::BTreeMap<usize, LazyContLine> = std::collections::BTreeMap::new();
898 if !self.config.allow_lazy_continuation {
899 let lazy_cont_lines = ctx.lazy_continuation_lines();
900 for lazy_info in lazy_cont_lines.iter() {
901 let line_num = lazy_info.line_num;
902 let is_within_block = list_blocks
904 .iter()
905 .any(|(start, end, _)| line_num >= *start && line_num <= *end);
906 if !is_within_block {
907 continue;
908 }
909 if !Self::should_apply_lazy_fix(ctx, line_num) {
911 continue;
912 }
913 lazy_fixes.insert(line_num, lazy_info.clone());
914 }
915 }
916
917 let mut insertions: std::collections::BTreeMap<usize, String> = std::collections::BTreeMap::new();
918
919 for &(start_line, end_line, ref prefix) in &list_blocks {
921 if ctx.inline_config().is_rule_disabled("MD032", start_line) {
923 continue;
924 }
925
926 if ctx
928 .line_info(start_line)
929 .is_some_and(|info| info.in_html_comment || info.in_mdx_comment)
930 {
931 continue;
932 }
933
934 if start_line > 1 {
936 let (content_line, has_blank_separation) = Self::find_preceding_content(ctx, start_line);
938
939 if !has_blank_separation && content_line > 0 {
941 let prev_line_str = lines[content_line - 1];
942 let is_prev_excluded = ctx
943 .line_info(content_line)
944 .is_some_and(|info| info.in_code_block || info.in_front_matter);
945 let prev_prefix = BLOCKQUOTE_PREFIX_RE.find(prev_line_str).map_or("", |m| m.as_str());
946
947 let should_require = Self::should_require_blank_line_before(ctx, content_line, start_line);
948 if !is_prev_excluded && prev_prefix.trim() == prefix.trim() && should_require {
950 let bq_prefix = ctx.blockquote_prefix_for_blank_line(start_line - 1);
952 insertions.insert(start_line, bq_prefix);
953 }
954 }
955 }
956
957 if end_line < num_lines {
959 let (content_line, has_blank_separation) = Self::find_following_content(ctx, end_line);
961
962 if !has_blank_separation && content_line > 0 {
964 let next_line_str = lines[content_line - 1];
965 let is_next_excluded = ctx
967 .line_info(content_line)
968 .is_some_and(|info| info.in_code_block || info.in_front_matter)
969 || (content_line <= ctx.lines.len()
970 && ctx.lines[content_line - 1].in_code_block
971 && ctx.lines[content_line - 1].indent >= 2
972 && (ctx.lines[content_line - 1]
973 .content(ctx.content)
974 .trim()
975 .starts_with("```")
976 || ctx.lines[content_line - 1]
977 .content(ctx.content)
978 .trim()
979 .starts_with("~~~")));
980 let next_prefix = BLOCKQUOTE_PREFIX_RE.find(next_line_str).map_or("", |m| m.as_str());
981
982 let end_line_str = lines[end_line - 1];
984 let end_line_prefix = BLOCKQUOTE_PREFIX_RE.find(end_line_str).map_or("", |m| m.as_str());
985 let end_line_bq_level = end_line_prefix.chars().filter(|&c| c == '>').count();
986 let next_line_bq_level = next_prefix.chars().filter(|&c| c == '>').count();
987 let exits_blockquote = end_line_bq_level > 0 && next_line_bq_level < end_line_bq_level;
988
989 if !is_next_excluded && next_prefix.trim() == prefix.trim() && !exits_blockquote {
992 let bq_prefix = ctx.blockquote_prefix_for_blank_line(end_line - 1);
994 insertions.insert(end_line + 1, bq_prefix);
995 }
996 }
997 }
998 }
999
1000 let mut result_lines: Vec<String> = Vec::with_capacity(num_lines + insertions.len());
1002 for (i, line) in lines.iter().enumerate() {
1003 let current_line_num = i + 1;
1004 if let Some(prefix_to_insert) = insertions.get(¤t_line_num)
1005 && (result_lines.is_empty() || result_lines.last().unwrap() != prefix_to_insert)
1006 {
1007 result_lines.push(prefix_to_insert.clone());
1008 }
1009
1010 if let Some(lazy_info) = lazy_fixes.get(¤t_line_num)
1012 && !ctx.inline_config().is_rule_disabled("MD032", current_line_num)
1013 {
1014 let fixed_line = Self::apply_lazy_fix_to_line(line, lazy_info);
1015 result_lines.push(fixed_line);
1016 } else {
1017 result_lines.push(line.to_string());
1018 }
1019 }
1020
1021 let mut result = result_lines.join("\n");
1023 if ctx.content.ends_with('\n') {
1024 result.push('\n');
1025 }
1026 result
1027 }
1028}
1029
1030fn is_blank_in_context(line: &str) -> bool {
1032 if let Some(m) = BLOCKQUOTE_PREFIX_RE.find(line) {
1035 line[m.end()..].trim().is_empty()
1037 } else {
1038 line.trim().is_empty()
1040 }
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use super::*;
1046 use crate::lint_context::LintContext;
1047 use crate::rule::Rule;
1048
1049 fn lint(content: &str) -> Vec<LintWarning> {
1050 let rule = MD032BlanksAroundLists::default();
1051 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1052 rule.check(&ctx).expect("Lint check failed")
1053 }
1054
1055 fn fix(content: &str) -> String {
1056 let rule = MD032BlanksAroundLists::default();
1057 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1058 rule.fix(&ctx).expect("Lint fix failed")
1059 }
1060
1061 #[test]
1062 fn test_fix_does_not_split_item_before_different_list_type() {
1063 let content = "- alpha beta\n aligned\n1. ordered item\n cont\n";
1067 assert_eq!(fix(content), "- alpha beta\n aligned\n\n1. ordered item\n cont\n");
1068
1069 let warnings = lint(content);
1072 assert_eq!(warnings.len(), 2);
1073 assert_eq!(warnings[0].line, 2);
1074 assert_eq!(warnings[1].line, 3);
1075 }
1076
1077 #[test]
1078 fn test_fix_does_not_split_blockquoted_item_before_different_list_type() {
1079 let content = "> - alpha beta\n> aligned\n> 1. ordered item\n";
1080 assert_eq!(fix(content), "> - alpha beta\n> aligned\n>\n> 1. ordered item\n");
1081 }
1082
1083 #[test]
1084 fn test_fix_keeps_lazy_continuation_with_its_item() {
1085 let content = "- alpha beta\nlazy\n1. ordered item\n";
1089 assert_eq!(fix(content), "- alpha beta\nlazy\n\n1. ordered item\n");
1090
1091 let warnings = lint(content);
1092 assert_eq!(warnings.len(), 2);
1093 assert_eq!(warnings[0].line, 2);
1094 assert_eq!(warnings[1].line, 3);
1095 }
1096
1097 #[test]
1098 fn test_fix_keeps_blockquoted_lazy_continuation_with_its_item() {
1099 let content = "> - alpha beta\n> lazy\n> 1. ordered item\n";
1100 assert_eq!(fix(content), "> - alpha beta\n> lazy\n>\n> 1. ordered item\n");
1101 }
1102
1103 #[test]
1104 fn test_fix_indents_lazy_continuation_when_not_allowed() {
1105 let rule = MD032BlanksAroundLists::from_config_struct(MD032Config {
1108 allow_lazy_continuation: false,
1109 });
1110 let content = "- alpha beta\nlazy\n1. ordered item\n";
1111 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1112 let fixed = rule.fix(&ctx).expect("Lint fix failed");
1113 assert_eq!(fixed, "- alpha beta\n lazy\n\n1. ordered item\n");
1114 }
1115
1116 fn check_warnings_have_fixes(content: &str) {
1118 let warnings = lint(content);
1119 for warning in &warnings {
1120 assert!(warning.fix.is_some(), "Warning should have fix: {warning:?}");
1121 }
1122 }
1123
1124 #[test]
1125 fn test_list_at_start() {
1126 let content = "- Item 1\n- Item 2\nText";
1129 let warnings = lint(content);
1130 assert_eq!(
1131 warnings.len(),
1132 0,
1133 "Trailing text is lazy continuation per CommonMark - no warning expected"
1134 );
1135 }
1136
1137 #[test]
1138 fn test_list_at_end() {
1139 let content = "Text\n- Item 1\n- Item 2";
1140 let warnings = lint(content);
1141 assert_eq!(
1142 warnings.len(),
1143 1,
1144 "Expected 1 warning for list at end without preceding blank line"
1145 );
1146 assert_eq!(
1147 warnings[0].line, 2,
1148 "Warning should be on the first line of the list (line 2)"
1149 );
1150 assert!(warnings[0].message.contains("preceded by blank line"));
1151
1152 check_warnings_have_fixes(content);
1154
1155 let fixed_content = fix(content);
1156 assert_eq!(fixed_content, "Text\n\n- Item 1\n- Item 2");
1157
1158 let warnings_after_fix = lint(&fixed_content);
1160 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1161 }
1162
1163 #[test]
1164 fn test_list_in_middle() {
1165 let content = "Text 1\n- Item 1\n- Item 2\nText 2";
1168 let warnings = lint(content);
1169 assert_eq!(
1170 warnings.len(),
1171 1,
1172 "Expected 1 warning for list needing preceding blank line (trailing text is lazy continuation)"
1173 );
1174 assert_eq!(warnings[0].line, 2, "Warning on line 2 (start)");
1175 assert!(warnings[0].message.contains("preceded by blank line"));
1176
1177 check_warnings_have_fixes(content);
1179
1180 let fixed_content = fix(content);
1181 assert_eq!(fixed_content, "Text 1\n\n- Item 1\n- Item 2\nText 2");
1182
1183 let warnings_after_fix = lint(&fixed_content);
1185 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1186 }
1187
1188 #[test]
1189 fn test_correct_spacing() {
1190 let content = "Text 1\n\n- Item 1\n- Item 2\n\nText 2";
1191 let warnings = lint(content);
1192 assert_eq!(warnings.len(), 0, "Expected no warnings for correctly spaced list");
1193
1194 let fixed_content = fix(content);
1195 assert_eq!(fixed_content, content, "Fix should not change correctly spaced content");
1196 }
1197
1198 #[test]
1199 fn test_list_with_content() {
1200 let content = "Text\n* Item 1\n Content\n* Item 2\n More content\nText";
1203 let warnings = lint(content);
1204 assert_eq!(
1205 warnings.len(),
1206 1,
1207 "Expected 1 warning for list needing preceding blank line. Got: {warnings:?}"
1208 );
1209 assert_eq!(warnings[0].line, 2, "Warning should be on line 2 (start)");
1210 assert!(warnings[0].message.contains("preceded by blank line"));
1211
1212 check_warnings_have_fixes(content);
1214
1215 let fixed_content = fix(content);
1216 let expected_fixed = "Text\n\n* Item 1\n Content\n* Item 2\n More content\nText";
1217 assert_eq!(
1218 fixed_content, expected_fixed,
1219 "Fix did not produce the expected output. Got:\n{fixed_content}"
1220 );
1221
1222 let warnings_after_fix = lint(&fixed_content);
1224 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1225 }
1226
1227 #[test]
1228 fn test_nested_list() {
1229 let content = "Text\n- Item 1\n - Nested 1\n- Item 2\nText";
1231 let warnings = lint(content);
1232 assert_eq!(
1233 warnings.len(),
1234 1,
1235 "Nested list block needs preceding blank only. Got: {warnings:?}"
1236 );
1237 assert_eq!(warnings[0].line, 2);
1238 assert!(warnings[0].message.contains("preceded by blank line"));
1239
1240 check_warnings_have_fixes(content);
1242
1243 let fixed_content = fix(content);
1244 assert_eq!(fixed_content, "Text\n\n- Item 1\n - Nested 1\n- Item 2\nText");
1245
1246 let warnings_after_fix = lint(&fixed_content);
1248 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1249 }
1250
1251 #[test]
1252 fn test_list_with_internal_blanks() {
1253 let content = "Text\n* Item 1\n\n More Item 1 Content\n* Item 2\nText";
1255 let warnings = lint(content);
1256 assert_eq!(
1257 warnings.len(),
1258 1,
1259 "List with internal blanks needs preceding blank only. Got: {warnings:?}"
1260 );
1261 assert_eq!(warnings[0].line, 2);
1262 assert!(warnings[0].message.contains("preceded by blank line"));
1263
1264 check_warnings_have_fixes(content);
1266
1267 let fixed_content = fix(content);
1268 assert_eq!(
1269 fixed_content,
1270 "Text\n\n* Item 1\n\n More Item 1 Content\n* Item 2\nText"
1271 );
1272
1273 let warnings_after_fix = lint(&fixed_content);
1275 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1276 }
1277
1278 #[test]
1279 fn test_ignore_code_blocks() {
1280 let content = "```\n- Not a list item\n```\nText";
1281 let warnings = lint(content);
1282 assert_eq!(warnings.len(), 0);
1283 let fixed_content = fix(content);
1284 assert_eq!(fixed_content, content);
1285 }
1286
1287 #[test]
1288 fn test_ignore_front_matter() {
1289 let content = "---\ntitle: Test\n---\n- List Item\nText";
1291 let warnings = lint(content);
1292 assert_eq!(
1293 warnings.len(),
1294 0,
1295 "Front matter test should have no MD032 warnings. Got: {warnings:?}"
1296 );
1297
1298 let fixed_content = fix(content);
1300 assert_eq!(fixed_content, content, "No changes when no warnings");
1301 }
1302
1303 #[test]
1304 fn test_multiple_lists() {
1305 let content = "Text\n- List 1 Item 1\n- List 1 Item 2\nText 2\n* List 2 Item 1\nText 3";
1310 let warnings = lint(content);
1311 assert!(
1313 !warnings.is_empty(),
1314 "Should have at least one warning for missing blank line. Got: {warnings:?}"
1315 );
1316
1317 check_warnings_have_fixes(content);
1319
1320 let fixed_content = fix(content);
1321 let warnings_after_fix = lint(&fixed_content);
1323 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1324 }
1325
1326 #[test]
1327 fn test_adjacent_lists() {
1328 let content = "- List 1\n\n* List 2";
1329 let warnings = lint(content);
1330 assert_eq!(warnings.len(), 0);
1331 let fixed_content = fix(content);
1332 assert_eq!(fixed_content, content);
1333 }
1334
1335 #[test]
1336 fn test_list_in_blockquote() {
1337 let content = "> Quote line 1\n> - List item 1\n> - List item 2\n> Quote line 2";
1339 let warnings = lint(content);
1340 assert_eq!(
1341 warnings.len(),
1342 1,
1343 "Expected 1 warning for blockquoted list needing preceding blank. Got: {warnings:?}"
1344 );
1345 assert_eq!(warnings[0].line, 2);
1346
1347 check_warnings_have_fixes(content);
1349
1350 let fixed_content = fix(content);
1351 assert_eq!(
1353 fixed_content, "> Quote line 1\n>\n> - List item 1\n> - List item 2\n> Quote line 2",
1354 "Fix for blockquoted list failed. Got:\n{fixed_content}"
1355 );
1356
1357 let warnings_after_fix = lint(&fixed_content);
1359 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1360 }
1361
1362 #[test]
1363 fn test_ordered_list() {
1364 let content = "Text\n1. Item 1\n2. Item 2\nText";
1366 let warnings = lint(content);
1367 assert_eq!(warnings.len(), 1);
1368
1369 check_warnings_have_fixes(content);
1371
1372 let fixed_content = fix(content);
1373 assert_eq!(fixed_content, "Text\n\n1. Item 1\n2. Item 2\nText");
1374
1375 let warnings_after_fix = lint(&fixed_content);
1377 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1378 }
1379
1380 #[test]
1381 fn test_no_double_blank_fix() {
1382 let content = "Text\n\n- Item 1\n- Item 2\nText"; let warnings = lint(content);
1385 assert_eq!(
1386 warnings.len(),
1387 0,
1388 "Should have no warnings - properly preceded, trailing is lazy"
1389 );
1390
1391 let fixed_content = fix(content);
1392 assert_eq!(
1393 fixed_content, content,
1394 "No fix needed when no warnings. Got:\n{fixed_content}"
1395 );
1396
1397 let content2 = "Text\n- Item 1\n- Item 2\n\nText"; let warnings2 = lint(content2);
1399 assert_eq!(warnings2.len(), 1);
1400 if !warnings2.is_empty() {
1401 assert_eq!(
1402 warnings2[0].line, 2,
1403 "Warning line for missing blank before should be the first line of the block"
1404 );
1405 }
1406
1407 check_warnings_have_fixes(content2);
1409
1410 let fixed_content2 = fix(content2);
1411 assert_eq!(
1412 fixed_content2, "Text\n\n- Item 1\n- Item 2\n\nText",
1413 "Fix added extra blank before. Got:\n{fixed_content2}"
1414 );
1415 }
1416
1417 #[test]
1418 fn test_empty_input() {
1419 let content = "";
1420 let warnings = lint(content);
1421 assert_eq!(warnings.len(), 0);
1422 let fixed_content = fix(content);
1423 assert_eq!(fixed_content, "");
1424 }
1425
1426 #[test]
1427 fn test_only_list() {
1428 let content = "- Item 1\n- Item 2";
1429 let warnings = lint(content);
1430 assert_eq!(warnings.len(), 0);
1431 let fixed_content = fix(content);
1432 assert_eq!(fixed_content, content);
1433 }
1434
1435 #[test]
1438 fn test_fix_complex_nested_blockquote() {
1439 let content = "> Text before\n> - Item 1\n> - Nested item\n> - Item 2\n> Text after";
1441 let warnings = lint(content);
1442 assert_eq!(
1443 warnings.len(),
1444 1,
1445 "Should warn for missing preceding blank only. Got: {warnings:?}"
1446 );
1447
1448 check_warnings_have_fixes(content);
1450
1451 let fixed_content = fix(content);
1452 let expected = "> Text before\n>\n> - Item 1\n> - Nested item\n> - Item 2\n> Text after";
1454 assert_eq!(fixed_content, expected, "Fix should preserve blockquote structure");
1455
1456 let warnings_after_fix = lint(&fixed_content);
1457 assert_eq!(warnings_after_fix.len(), 0, "Fix should eliminate all warnings");
1458 }
1459
1460 #[test]
1461 fn test_fix_mixed_list_markers() {
1462 let content = "Text\n- Item 1\n* Item 2\n+ Item 3\nText";
1465 let warnings = lint(content);
1466 assert!(
1468 !warnings.is_empty(),
1469 "Should have at least 1 warning for mixed marker list. Got: {warnings:?}"
1470 );
1471
1472 check_warnings_have_fixes(content);
1474
1475 let fixed_content = fix(content);
1476 assert!(
1478 fixed_content.contains("Text\n\n-"),
1479 "Fix should add blank line before first list item"
1480 );
1481
1482 let warnings_after_fix = lint(&fixed_content);
1484 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1485 }
1486
1487 #[test]
1488 fn test_fix_ordered_list_with_different_numbers() {
1489 let content = "Text\n1. First\n3. Third\n2. Second\nText";
1491 let warnings = lint(content);
1492 assert_eq!(warnings.len(), 1, "Should warn for missing preceding blank only");
1493
1494 check_warnings_have_fixes(content);
1496
1497 let fixed_content = fix(content);
1498 let expected = "Text\n\n1. First\n3. Third\n2. Second\nText";
1499 assert_eq!(
1500 fixed_content, expected,
1501 "Fix should handle ordered lists with non-sequential numbers"
1502 );
1503
1504 let warnings_after_fix = lint(&fixed_content);
1506 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1507 }
1508
1509 #[test]
1510 fn test_fix_list_with_code_blocks_inside() {
1511 let content = "Text\n- Item 1\n ```\n code\n ```\n- Item 2\nText";
1513 let warnings = lint(content);
1514 assert_eq!(warnings.len(), 1, "Should warn for missing preceding blank only");
1515
1516 check_warnings_have_fixes(content);
1518
1519 let fixed_content = fix(content);
1520 let expected = "Text\n\n- Item 1\n ```\n code\n ```\n- Item 2\nText";
1521 assert_eq!(
1522 fixed_content, expected,
1523 "Fix should handle lists with internal code blocks"
1524 );
1525
1526 let warnings_after_fix = lint(&fixed_content);
1528 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1529 }
1530
1531 #[test]
1532 fn test_fix_deeply_nested_lists() {
1533 let content = "Text\n- Level 1\n - Level 2\n - Level 3\n - Level 4\n- Back to Level 1\nText";
1535 let warnings = lint(content);
1536 assert_eq!(warnings.len(), 1, "Should warn for missing preceding blank only");
1537
1538 check_warnings_have_fixes(content);
1540
1541 let fixed_content = fix(content);
1542 let expected = "Text\n\n- Level 1\n - Level 2\n - Level 3\n - Level 4\n- Back to Level 1\nText";
1543 assert_eq!(fixed_content, expected, "Fix should handle deeply nested lists");
1544
1545 let warnings_after_fix = lint(&fixed_content);
1547 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1548 }
1549
1550 #[test]
1551 fn test_fix_list_with_multiline_items() {
1552 let content = "Text\n- Item 1\n continues here\n and here\n- Item 2\n also continues\nText";
1555 let warnings = lint(content);
1556 assert_eq!(
1557 warnings.len(),
1558 1,
1559 "Should only warn for missing blank before list (trailing text is lazy continuation)"
1560 );
1561
1562 check_warnings_have_fixes(content);
1564
1565 let fixed_content = fix(content);
1566 let expected = "Text\n\n- Item 1\n continues here\n and here\n- Item 2\n also continues\nText";
1567 assert_eq!(fixed_content, expected, "Fix should add blank before list only");
1568
1569 let warnings_after_fix = lint(&fixed_content);
1571 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1572 }
1573
1574 #[test]
1575 fn test_fix_list_at_document_boundaries() {
1576 let content1 = "- Item 1\n- Item 2";
1578 let warnings1 = lint(content1);
1579 assert_eq!(
1580 warnings1.len(),
1581 0,
1582 "List at document start should not need blank before"
1583 );
1584 let fixed1 = fix(content1);
1585 assert_eq!(fixed1, content1, "No fix needed for list at start");
1586
1587 let content2 = "Text\n- Item 1\n- Item 2";
1589 let warnings2 = lint(content2);
1590 assert_eq!(warnings2.len(), 1, "List at document end should need blank before");
1591 check_warnings_have_fixes(content2);
1592 let fixed2 = fix(content2);
1593 assert_eq!(
1594 fixed2, "Text\n\n- Item 1\n- Item 2",
1595 "Should add blank before list at end"
1596 );
1597 }
1598
1599 #[test]
1600 fn test_fix_preserves_existing_blank_lines() {
1601 let content = "Text\n\n\n- Item 1\n- Item 2\n\n\nText";
1602 let warnings = lint(content);
1603 assert_eq!(warnings.len(), 0, "Multiple blank lines should be preserved");
1604 let fixed_content = fix(content);
1605 assert_eq!(fixed_content, content, "Fix should not modify already correct content");
1606 }
1607
1608 #[test]
1609 fn test_fix_handles_tabs_and_spaces() {
1610 let content = "Text\n\t- Item with tab\n - Item with spaces\nText";
1613 let warnings = lint(content);
1614 assert!(!warnings.is_empty(), "Should warn for missing blank before list");
1616
1617 check_warnings_have_fixes(content);
1619
1620 let fixed_content = fix(content);
1621 let expected = "Text\n\t- Item with tab\n\n - Item with spaces\nText";
1624 assert_eq!(fixed_content, expected, "Fix should add blank before list item");
1625
1626 let warnings_after_fix = lint(&fixed_content);
1628 assert_eq!(warnings_after_fix.len(), 0, "Fix should resolve all warnings");
1629 }
1630
1631 #[test]
1632 fn test_fix_warning_objects_have_correct_ranges() {
1633 let content = "Text\n- Item 1\n- Item 2\nText";
1635 let warnings = lint(content);
1636 assert_eq!(warnings.len(), 1, "Only preceding blank warning expected");
1637
1638 for warning in &warnings {
1640 assert!(warning.fix.is_some(), "Warning should have fix");
1641 let fix = warning.fix.as_ref().unwrap();
1642 assert!(fix.range.start <= fix.range.end, "Fix range should be valid");
1643 assert!(
1644 !fix.replacement.is_empty() || fix.range.start == fix.range.end,
1645 "Fix should have replacement or be insertion"
1646 );
1647 }
1648 }
1649
1650 #[test]
1651 fn test_fix_idempotent() {
1652 let content = "Text\n- Item 1\n- Item 2\nText";
1654
1655 let fixed_once = fix(content);
1657 assert_eq!(fixed_once, "Text\n\n- Item 1\n- Item 2\nText");
1658
1659 let fixed_twice = fix(&fixed_once);
1661 assert_eq!(fixed_twice, fixed_once, "Fix should be idempotent");
1662
1663 let warnings_after_fix = lint(&fixed_once);
1665 assert_eq!(warnings_after_fix.len(), 0, "No warnings should remain after fix");
1666 }
1667
1668 #[test]
1669 fn test_fix_with_normalized_line_endings() {
1670 let content = "Text\n- Item 1\n- Item 2\nText";
1674 let warnings = lint(content);
1675 assert_eq!(warnings.len(), 1, "Should detect missing blank before list");
1676
1677 check_warnings_have_fixes(content);
1679
1680 let fixed_content = fix(content);
1681 let expected = "Text\n\n- Item 1\n- Item 2\nText";
1683 assert_eq!(fixed_content, expected, "Fix should work with normalized LF content");
1684 }
1685
1686 #[test]
1687 fn test_fix_preserves_final_newline() {
1688 let content_with_newline = "Text\n- Item 1\n- Item 2\nText\n";
1691 let fixed_with_newline = fix(content_with_newline);
1692 assert!(
1693 fixed_with_newline.ends_with('\n'),
1694 "Fix should preserve final newline when present"
1695 );
1696 assert_eq!(fixed_with_newline, "Text\n\n- Item 1\n- Item 2\nText\n");
1698
1699 let content_without_newline = "Text\n- Item 1\n- Item 2\nText";
1701 let fixed_without_newline = fix(content_without_newline);
1702 assert!(
1703 !fixed_without_newline.ends_with('\n'),
1704 "Fix should not add final newline when not present"
1705 );
1706 assert_eq!(fixed_without_newline, "Text\n\n- Item 1\n- Item 2\nText");
1708 }
1709
1710 #[test]
1711 fn test_fix_multiline_list_items_no_indent() {
1712 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";
1713
1714 let warnings = lint(content);
1715 assert_eq!(
1717 warnings.len(),
1718 0,
1719 "Should not warn for properly formatted list with multi-line items. Got: {warnings:?}"
1720 );
1721
1722 let fixed_content = fix(content);
1723 assert_eq!(
1725 fixed_content, content,
1726 "Should not modify correctly formatted multi-line list items"
1727 );
1728 }
1729
1730 #[test]
1731 fn test_nested_list_with_lazy_continuation() {
1732 let content = r#"# Test
1738
1739- **Token Dispatch (Phase 3.2)**: COMPLETE. Extracts tokens from both:
1740 1. Switch/case dispatcher statements (original Phase 3.2)
1741 2. Inline conditionals - if/else, bitwise checks (`&`, `|`), comparison (`==`,
1742`!=`), ternary operators (`?:`), macros (`ISTOK`, `ISUNSET`), compound conditions (`&&`, `||`) (Phase 3.2.1)
1743 - 30 explicit tokens extracted, 23 dispatcher rules with embedded token
1744 references"#;
1745
1746 let warnings = lint(content);
1747 let md032_warnings: Vec<_> = warnings
1750 .iter()
1751 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1752 .collect();
1753 assert_eq!(
1754 md032_warnings.len(),
1755 0,
1756 "Should not warn for nested list with lazy continuation. Got: {md032_warnings:?}"
1757 );
1758 }
1759
1760 #[test]
1761 fn test_pipes_in_code_spans_not_detected_as_table() {
1762 let content = r#"# Test
1764
1765- Item with `a | b` inline code
1766 - Nested item should work
1767
1768"#;
1769
1770 let warnings = lint(content);
1771 let md032_warnings: Vec<_> = warnings
1772 .iter()
1773 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1774 .collect();
1775 assert_eq!(
1776 md032_warnings.len(),
1777 0,
1778 "Pipes in code spans should not break lists. Got: {md032_warnings:?}"
1779 );
1780 }
1781
1782 #[test]
1783 fn test_multiple_code_spans_with_pipes() {
1784 let content = r#"# Test
1786
1787- Item with `a | b` and `c || d` operators
1788 - Nested item should work
1789
1790"#;
1791
1792 let warnings = lint(content);
1793 let md032_warnings: Vec<_> = warnings
1794 .iter()
1795 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1796 .collect();
1797 assert_eq!(
1798 md032_warnings.len(),
1799 0,
1800 "Multiple code spans with pipes should not break lists. Got: {md032_warnings:?}"
1801 );
1802 }
1803
1804 #[test]
1805 fn test_actual_table_breaks_list() {
1806 let content = r#"# Test
1808
1809- Item before table
1810
1811| Col1 | Col2 |
1812|------|------|
1813| A | B |
1814
1815- Item after table
1816
1817"#;
1818
1819 let warnings = lint(content);
1820 let md032_warnings: Vec<_> = warnings
1822 .iter()
1823 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1824 .collect();
1825 assert_eq!(
1826 md032_warnings.len(),
1827 0,
1828 "Both lists should be properly separated by blank lines. Got: {md032_warnings:?}"
1829 );
1830 }
1831
1832 #[test]
1833 fn test_thematic_break_not_lazy_continuation() {
1834 let content = r#"- Item 1
1837- Item 2
1838***
1839
1840More text.
1841"#;
1842
1843 let warnings = lint(content);
1844 let md032_warnings: Vec<_> = warnings
1845 .iter()
1846 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1847 .collect();
1848 assert_eq!(
1849 md032_warnings.len(),
1850 1,
1851 "Should warn for list not followed by blank line before thematic break. Got: {md032_warnings:?}"
1852 );
1853 assert!(
1854 md032_warnings[0].message.contains("followed by blank line"),
1855 "Warning should be about missing blank after list"
1856 );
1857 }
1858
1859 #[test]
1860 fn test_thematic_break_with_blank_line() {
1861 let content = r#"- Item 1
1863- Item 2
1864
1865***
1866
1867More text.
1868"#;
1869
1870 let warnings = lint(content);
1871 let md032_warnings: Vec<_> = warnings
1872 .iter()
1873 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1874 .collect();
1875 assert_eq!(
1876 md032_warnings.len(),
1877 0,
1878 "Should not warn when list is properly followed by blank line. Got: {md032_warnings:?}"
1879 );
1880 }
1881
1882 #[test]
1883 fn test_various_thematic_break_styles() {
1884 for hr in ["---", "***", "___"] {
1889 let content = format!(
1890 r#"- Item 1
1891- Item 2
1892{hr}
1893
1894More text.
1895"#
1896 );
1897
1898 let warnings = lint(&content);
1899 let md032_warnings: Vec<_> = warnings
1900 .iter()
1901 .filter(|w| w.rule_name.as_deref() == Some("MD032"))
1902 .collect();
1903 assert_eq!(
1904 md032_warnings.len(),
1905 1,
1906 "Should warn for HR style '{hr}' without blank line. Got: {md032_warnings:?}"
1907 );
1908 }
1909 }
1910
1911 fn lint_with_config(content: &str, config: MD032Config) -> Vec<LintWarning> {
1914 let rule = MD032BlanksAroundLists::from_config_struct(config);
1915 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1916 rule.check(&ctx).expect("Lint check failed")
1917 }
1918
1919 fn fix_with_config(content: &str, config: MD032Config) -> String {
1920 let rule = MD032BlanksAroundLists::from_config_struct(config);
1921 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1922 rule.fix(&ctx).expect("Lint fix failed")
1923 }
1924
1925 #[test]
1926 fn test_lazy_continuation_allowed_by_default() {
1927 let content = "# Heading\n\n1. List\nSome text.";
1929 let warnings = lint(content);
1930 assert_eq!(
1931 warnings.len(),
1932 0,
1933 "Default behavior should allow lazy continuation. Got: {warnings:?}"
1934 );
1935 }
1936
1937 #[test]
1938 fn test_lazy_continuation_disallowed() {
1939 let content = "# Heading\n\n1. List\nSome text.";
1941 let config = MD032Config {
1942 allow_lazy_continuation: false,
1943 };
1944 let warnings = lint_with_config(content, config);
1945 assert_eq!(
1946 warnings.len(),
1947 1,
1948 "Should warn when lazy continuation is disallowed. Got: {warnings:?}"
1949 );
1950 assert!(
1951 warnings[0].message.contains("Lazy continuation"),
1952 "Warning message should mention lazy continuation"
1953 );
1954 assert_eq!(warnings[0].line, 4, "Warning should be on the lazy line");
1955 }
1956
1957 #[test]
1958 fn test_lazy_continuation_fix() {
1959 let content = "# Heading\n\n1. List\nSome text.";
1961 let config = MD032Config {
1962 allow_lazy_continuation: false,
1963 };
1964 let fixed = fix_with_config(content, config.clone());
1965 assert_eq!(
1967 fixed, "# Heading\n\n1. List\n Some text.",
1968 "Fix should add proper indentation to lazy continuation"
1969 );
1970
1971 let warnings_after = lint_with_config(&fixed, config);
1973 assert_eq!(warnings_after.len(), 0, "No warnings should remain after fix");
1974 }
1975
1976 #[test]
1977 fn test_lazy_continuation_multiple_lines() {
1978 let content = "- Item 1\nLine 2\nLine 3";
1980 let config = MD032Config {
1981 allow_lazy_continuation: false,
1982 };
1983 let warnings = lint_with_config(content, config.clone());
1984 assert_eq!(
1986 warnings.len(),
1987 2,
1988 "Should warn for each lazy continuation line. Got: {warnings:?}"
1989 );
1990
1991 let fixed = fix_with_config(content, config.clone());
1992 assert_eq!(
1994 fixed, "- Item 1\n Line 2\n Line 3",
1995 "Fix should add proper indentation to lazy continuation lines"
1996 );
1997
1998 let warnings_after = lint_with_config(&fixed, config);
2000 assert_eq!(warnings_after.len(), 0, "No warnings should remain after fix");
2001 }
2002
2003 #[test]
2004 fn test_lazy_continuation_with_indented_content() {
2005 let content = "- Item 1\n Indented content\nLazy text";
2007 let config = MD032Config {
2008 allow_lazy_continuation: false,
2009 };
2010 let warnings = lint_with_config(content, config);
2011 assert_eq!(
2012 warnings.len(),
2013 1,
2014 "Should warn for lazy text after indented content. Got: {warnings:?}"
2015 );
2016 }
2017
2018 #[test]
2019 fn test_lazy_continuation_properly_separated() {
2020 let content = "- Item 1\n\nSome text.";
2022 let config = MD032Config {
2023 allow_lazy_continuation: false,
2024 };
2025 let warnings = lint_with_config(content, config);
2026 assert_eq!(
2027 warnings.len(),
2028 0,
2029 "Should not warn when list is properly followed by blank line. Got: {warnings:?}"
2030 );
2031 }
2032
2033 #[test]
2036 fn test_lazy_continuation_ordered_list_parenthesis_marker() {
2037 let content = "1) First item\nLazy continuation";
2039 let config = MD032Config {
2040 allow_lazy_continuation: false,
2041 };
2042 let warnings = lint_with_config(content, config.clone());
2043 assert_eq!(
2044 warnings.len(),
2045 1,
2046 "Should warn for lazy continuation with parenthesis marker"
2047 );
2048
2049 let fixed = fix_with_config(content, config);
2050 assert_eq!(fixed, "1) First item\n Lazy continuation");
2052 }
2053
2054 #[test]
2055 fn test_lazy_continuation_followed_by_another_list() {
2056 let content = "- Item 1\nSome text\n- Item 2";
2062 let config = MD032Config {
2063 allow_lazy_continuation: false,
2064 };
2065 let warnings = lint_with_config(content, config);
2066 assert_eq!(
2068 warnings.len(),
2069 1,
2070 "Should warn about lazy continuation within list. Got: {warnings:?}"
2071 );
2072 assert!(
2073 warnings[0].message.contains("Lazy continuation"),
2074 "Warning should be about lazy continuation"
2075 );
2076 assert_eq!(warnings[0].line, 2, "Warning should be on line 2");
2077 }
2078
2079 #[test]
2080 fn test_lazy_continuation_multiple_in_document() {
2081 let content = "- Item 1\nLazy 1\n\n- Item 2\nLazy 2";
2086 let config = MD032Config {
2087 allow_lazy_continuation: false,
2088 };
2089 let warnings = lint_with_config(content, config.clone());
2090 assert_eq!(
2092 warnings.len(),
2093 2,
2094 "Should warn for both lazy continuations. Got: {warnings:?}"
2095 );
2096
2097 let fixed = fix_with_config(content, config.clone());
2098 assert!(
2100 fixed.contains(" Lazy 1"),
2101 "Fixed content should have indented 'Lazy 1'. Got: {fixed:?}"
2102 );
2103 assert!(
2104 fixed.contains(" Lazy 2"),
2105 "Fixed content should have indented 'Lazy 2'. Got: {fixed:?}"
2106 );
2107
2108 let warnings_after = lint_with_config(&fixed, config);
2109 assert_eq!(
2111 warnings_after.len(),
2112 0,
2113 "All warnings should be fixed after auto-fix. Got: {warnings_after:?}"
2114 );
2115 }
2116
2117 #[test]
2118 fn test_lazy_continuation_end_of_document_no_newline() {
2119 let content = "- Item\nNo trailing newline";
2121 let config = MD032Config {
2122 allow_lazy_continuation: false,
2123 };
2124 let warnings = lint_with_config(content, config.clone());
2125 assert_eq!(warnings.len(), 1, "Should warn even at end of document");
2126
2127 let fixed = fix_with_config(content, config);
2128 assert_eq!(fixed, "- Item\n No trailing newline");
2130 }
2131
2132 #[test]
2133 fn test_lazy_continuation_thematic_break_still_needs_blank() {
2134 let content = "- Item 1\n---";
2137 let config = MD032Config {
2138 allow_lazy_continuation: false,
2139 };
2140 let warnings = lint_with_config(content, config.clone());
2141 assert_eq!(
2143 warnings.len(),
2144 1,
2145 "List should need blank line before thematic break. Got: {warnings:?}"
2146 );
2147
2148 let fixed = fix_with_config(content, config);
2150 assert_eq!(fixed, "- Item 1\n\n---");
2151 }
2152
2153 #[test]
2154 fn test_lazy_continuation_heading_not_flagged() {
2155 let content = "- Item 1\n# Heading";
2158 let config = MD032Config {
2159 allow_lazy_continuation: false,
2160 };
2161 let warnings = lint_with_config(content, config);
2162 assert!(
2165 warnings.iter().all(|w| !w.message.contains("lazy")),
2166 "Heading should not trigger lazy continuation warning"
2167 );
2168 }
2169
2170 #[test]
2171 fn test_lazy_continuation_mixed_list_types() {
2172 let content = "- Unordered\n1. Ordered\nLazy text";
2174 let config = MD032Config {
2175 allow_lazy_continuation: false,
2176 };
2177 let warnings = lint_with_config(content, config.clone());
2178 assert!(!warnings.is_empty(), "Should warn about structure issues");
2179 }
2180
2181 #[test]
2182 fn test_lazy_continuation_deep_nesting() {
2183 let content = "- Level 1\n - Level 2\n - Level 3\nLazy at root";
2185 let config = MD032Config {
2186 allow_lazy_continuation: false,
2187 };
2188 let warnings = lint_with_config(content, config.clone());
2189 assert!(
2190 !warnings.is_empty(),
2191 "Should warn about lazy continuation after nested list"
2192 );
2193
2194 let fixed = fix_with_config(content, config.clone());
2195 let warnings_after = lint_with_config(&fixed, config);
2196 assert_eq!(warnings_after.len(), 0, "No warnings should remain after fix");
2197 }
2198
2199 #[test]
2200 fn test_lazy_continuation_with_emphasis_in_text() {
2201 let content = "- Item\n*emphasized* continuation";
2203 let config = MD032Config {
2204 allow_lazy_continuation: false,
2205 };
2206 let warnings = lint_with_config(content, config.clone());
2207 assert_eq!(warnings.len(), 1, "Should warn even with emphasis in continuation");
2208
2209 let fixed = fix_with_config(content, config);
2210 assert_eq!(fixed, "- Item\n *emphasized* continuation");
2212 }
2213
2214 #[test]
2215 fn test_lazy_continuation_with_code_span() {
2216 let content = "- Item\n`code` continuation";
2218 let config = MD032Config {
2219 allow_lazy_continuation: false,
2220 };
2221 let warnings = lint_with_config(content, config.clone());
2222 assert_eq!(warnings.len(), 1, "Should warn even with code span in continuation");
2223
2224 let fixed = fix_with_config(content, config);
2225 assert_eq!(fixed, "- Item\n `code` continuation");
2227 }
2228
2229 #[test]
2236 fn test_issue295_case1_nested_bullets_then_continuation_then_item() {
2237 let content = r#"1. Create a new Chat conversation:
2240 - On the sidebar, select **New Chat**.
2241 - In the box, type `/new`.
2242 A new Chat conversation replaces the previous one.
22431. Under the Chat text box, turn off the toggle."#;
2244 let config = MD032Config {
2245 allow_lazy_continuation: false,
2246 };
2247 let warnings = lint_with_config(content, config);
2248 let lazy_warnings: Vec<_> = warnings
2250 .iter()
2251 .filter(|w| w.message.contains("Lazy continuation"))
2252 .collect();
2253 assert!(
2254 !lazy_warnings.is_empty(),
2255 "Should detect lazy continuation after nested bullets. Got: {warnings:?}"
2256 );
2257 assert!(
2258 lazy_warnings.iter().any(|w| w.line == 4),
2259 "Should warn on line 4. Got: {lazy_warnings:?}"
2260 );
2261 }
2262
2263 #[test]
2264 fn test_issue295_case3_code_span_starts_lazy_continuation() {
2265 let content = r#"- `field`: Is the specific key:
2268 - `password`: Accesses the password.
2269 - `api_key`: Accesses the api_key.
2270 `token`: Specifies which ID token to use.
2271- `version_id`: Is the unique identifier."#;
2272 let config = MD032Config {
2273 allow_lazy_continuation: false,
2274 };
2275 let warnings = lint_with_config(content, config);
2276 let lazy_warnings: Vec<_> = warnings
2278 .iter()
2279 .filter(|w| w.message.contains("Lazy continuation"))
2280 .collect();
2281 assert!(
2282 !lazy_warnings.is_empty(),
2283 "Should detect lazy continuation starting with code span. Got: {warnings:?}"
2284 );
2285 assert!(
2286 lazy_warnings.iter().any(|w| w.line == 4),
2287 "Should warn on line 4 (code span start). Got: {lazy_warnings:?}"
2288 );
2289 }
2290
2291 #[test]
2292 fn test_issue295_case4_deep_nesting_with_continuation_then_item() {
2293 let content = r#"- Check out the branch, and test locally.
2295 - If the MR requires significant modifications:
2296 - **Skip local testing** and review instead.
2297 - **Request verification** from the author.
2298 - **Identify the minimal change** needed.
2299 Your testing might result in opportunities.
2300- If you don't understand, _say so_."#;
2301 let config = MD032Config {
2302 allow_lazy_continuation: false,
2303 };
2304 let warnings = lint_with_config(content, config);
2305 let lazy_warnings: Vec<_> = warnings
2307 .iter()
2308 .filter(|w| w.message.contains("Lazy continuation"))
2309 .collect();
2310 assert!(
2311 !lazy_warnings.is_empty(),
2312 "Should detect lazy continuation after deep nesting. Got: {warnings:?}"
2313 );
2314 assert!(
2315 lazy_warnings.iter().any(|w| w.line == 6),
2316 "Should warn on line 6. Got: {lazy_warnings:?}"
2317 );
2318 }
2319
2320 #[test]
2321 fn test_issue295_ordered_list_nested_bullets_continuation() {
2322 let content = r#"# Test
2325
23261. First item.
2327 - Nested A.
2328 - Nested B.
2329 Continuation at outer level.
23301. Second item."#;
2331 let config = MD032Config {
2332 allow_lazy_continuation: false,
2333 };
2334 let warnings = lint_with_config(content, config);
2335 let lazy_warnings: Vec<_> = warnings
2337 .iter()
2338 .filter(|w| w.message.contains("Lazy continuation"))
2339 .collect();
2340 assert!(
2341 !lazy_warnings.is_empty(),
2342 "Should detect lazy continuation at outer level after nested. Got: {warnings:?}"
2343 );
2344 assert!(
2346 lazy_warnings.iter().any(|w| w.line == 6),
2347 "Should warn on line 6. Got: {lazy_warnings:?}"
2348 );
2349 }
2350
2351 #[test]
2352 fn test_issue295_multiple_lazy_lines_after_nested() {
2353 let content = r#"1. The device client receives a response.
2355 - Those defined by OAuth Framework.
2356 - Those specific to device authorization.
2357 Those error responses are described below.
2358 For more information on each response,
2359 see the documentation.
23601. Next step in the process."#;
2361 let config = MD032Config {
2362 allow_lazy_continuation: false,
2363 };
2364 let warnings = lint_with_config(content, config);
2365 let lazy_warnings: Vec<_> = warnings
2367 .iter()
2368 .filter(|w| w.message.contains("Lazy continuation"))
2369 .collect();
2370 assert!(
2371 lazy_warnings.len() >= 3,
2372 "Should detect multiple lazy continuation lines. Got {} warnings: {lazy_warnings:?}",
2373 lazy_warnings.len()
2374 );
2375 }
2376
2377 #[test]
2378 fn test_issue295_properly_indented_not_lazy() {
2379 let content = r#"1. First item.
2381 - Nested A.
2382 - Nested B.
2383
2384 Properly indented continuation.
23851. Second item."#;
2386 let config = MD032Config {
2387 allow_lazy_continuation: false,
2388 };
2389 let warnings = lint_with_config(content, config);
2390 let lazy_warnings: Vec<_> = warnings
2392 .iter()
2393 .filter(|w| w.message.contains("Lazy continuation"))
2394 .collect();
2395 assert_eq!(
2396 lazy_warnings.len(),
2397 0,
2398 "Should NOT warn when blank line separates continuation. Got: {lazy_warnings:?}"
2399 );
2400 }
2401
2402 #[test]
2409 fn test_html_comment_before_list_with_preceding_blank() {
2410 let content = "Some text.\n\n<!-- comment -->\n- List item";
2413 let warnings = lint(content);
2414 assert_eq!(
2415 warnings.len(),
2416 0,
2417 "Should not warn when blank line exists before HTML comment. Got: {warnings:?}"
2418 );
2419 }
2420
2421 #[test]
2422 fn test_html_comment_after_list_with_following_blank() {
2423 let content = "- List item\n<!-- comment -->\n\nSome text.";
2425 let warnings = lint(content);
2426 assert_eq!(
2427 warnings.len(),
2428 0,
2429 "Should not warn when blank line exists after HTML comment. Got: {warnings:?}"
2430 );
2431 }
2432
2433 #[test]
2434 fn test_list_inside_html_comment_ignored() {
2435 let content = "<!--\n1. First\n2. Second\n3. Third\n-->";
2437 let warnings = lint(content);
2438 assert_eq!(
2439 warnings.len(),
2440 0,
2441 "Should not analyze lists inside HTML comments. Got: {warnings:?}"
2442 );
2443 }
2444
2445 #[test]
2446 fn test_multiline_html_comment_before_list() {
2447 let content = "Text\n\n<!--\nThis is a\nmulti-line\ncomment\n-->\n- Item";
2449 let warnings = lint(content);
2450 assert_eq!(
2451 warnings.len(),
2452 0,
2453 "Multi-line HTML comment should be transparent. Got: {warnings:?}"
2454 );
2455 }
2456
2457 #[test]
2458 fn test_no_blank_before_html_comment_still_warns() {
2459 let content = "Some text.\n<!-- comment -->\n- List item";
2461 let warnings = lint(content);
2462 assert_eq!(
2463 warnings.len(),
2464 1,
2465 "Should warn when no blank line exists (even with HTML comment). Got: {warnings:?}"
2466 );
2467 assert!(
2468 warnings[0].message.contains("preceded by blank line"),
2469 "Should be 'preceded by blank line' warning"
2470 );
2471 }
2472
2473 #[test]
2474 fn test_no_blank_after_html_comment_no_warn_lazy_continuation() {
2475 let content = "- List item\n<!-- comment -->\nSome text.";
2478 let warnings = lint(content);
2479 assert_eq!(
2480 warnings.len(),
2481 0,
2482 "Should not warn - text after comment becomes lazy continuation. Got: {warnings:?}"
2483 );
2484 }
2485
2486 #[test]
2487 fn test_list_followed_by_heading_through_comment_should_warn() {
2488 let content = "- List item\n<!-- comment -->\n# Heading";
2490 let warnings = lint(content);
2491 assert!(
2494 warnings.len() <= 1,
2495 "Should handle heading after comment gracefully. Got: {warnings:?}"
2496 );
2497 }
2498
2499 #[test]
2500 fn test_html_comment_between_list_and_text_both_directions() {
2501 let content = "Text before.\n\n<!-- comment -->\n- Item 1\n- Item 2\n<!-- another -->\n\nText after.";
2503 let warnings = lint(content);
2504 assert_eq!(
2505 warnings.len(),
2506 0,
2507 "Should not warn with proper separation through comments. Got: {warnings:?}"
2508 );
2509 }
2510
2511 #[test]
2512 fn test_html_comment_fix_does_not_insert_unnecessary_blank() {
2513 let content = "Text.\n\n<!-- comment -->\n- Item";
2515 let fixed = fix(content);
2516 assert_eq!(fixed, content, "Fix should not modify already-correct content");
2517 }
2518
2519 #[test]
2520 fn test_html_comment_fix_adds_blank_when_needed() {
2521 let content = "Text.\n<!-- comment -->\n- Item";
2524 let fixed = fix(content);
2525 assert!(
2526 fixed.contains("<!-- comment -->\n\n- Item"),
2527 "Fix should add blank line before list. Got: {fixed}"
2528 );
2529 }
2530
2531 #[test]
2532 fn test_ordered_list_inside_html_comment() {
2533 let content = "<!--\n3. Starting at 3\n4. Next item\n-->";
2535 let warnings = lint(content);
2536 assert_eq!(
2537 warnings.len(),
2538 0,
2539 "Should not warn about ordered list inside HTML comment. Got: {warnings:?}"
2540 );
2541 }
2542
2543 #[test]
2550 fn test_blockquote_list_exit_no_warning() {
2551 let content = "- outer item\n > - blockquote list 1\n > - blockquote list 2\n- next outer item";
2553 let warnings = lint(content);
2554 assert_eq!(
2555 warnings.len(),
2556 0,
2557 "Should not warn when exiting blockquote. Got: {warnings:?}"
2558 );
2559 }
2560
2561 #[test]
2562 fn test_nested_blockquote_list_exit() {
2563 let content = "- outer\n - nested\n > - bq list 1\n > - bq list 2\n - back to nested\n- outer again";
2565 let warnings = lint(content);
2566 assert_eq!(
2567 warnings.len(),
2568 0,
2569 "Should not warn when exiting nested blockquote list. Got: {warnings:?}"
2570 );
2571 }
2572
2573 #[test]
2574 fn test_blockquote_same_level_no_warning() {
2575 let content = "> - item 1\n> - item 2\n> Text after";
2578 let warnings = lint(content);
2579 assert_eq!(
2580 warnings.len(),
2581 0,
2582 "Should not warn - text is lazy continuation in blockquote. Got: {warnings:?}"
2583 );
2584 }
2585
2586 #[test]
2587 fn test_blockquote_list_with_special_chars() {
2588 let content = "- Item with <>&\n > - blockquote item\n- Back to outer";
2590 let warnings = lint(content);
2591 assert_eq!(
2592 warnings.len(),
2593 0,
2594 "Special chars in content should not affect blockquote detection. Got: {warnings:?}"
2595 );
2596 }
2597
2598 #[test]
2599 fn test_lazy_continuation_whitespace_only_line() {
2600 let content = "- Item\n \nText after whitespace-only line";
2603 let config = MD032Config {
2604 allow_lazy_continuation: false,
2605 };
2606 let warnings = lint_with_config(content, config);
2607 assert_eq!(
2609 warnings.len(),
2610 0,
2611 "Whitespace-only line IS a separator in CommonMark. Got: {warnings:?}"
2612 );
2613 }
2614
2615 #[test]
2616 fn test_lazy_continuation_blockquote_context() {
2617 let content = "> - Item\n> Lazy in quote";
2619 let config = MD032Config {
2620 allow_lazy_continuation: false,
2621 };
2622 let warnings = lint_with_config(content, config);
2623 assert!(warnings.len() <= 1, "Should handle blockquote context gracefully");
2626 }
2627
2628 #[test]
2629 fn test_lazy_continuation_fix_preserves_content() {
2630 let content = "- Item with special chars: <>&\nContinuation with: \"quotes\"";
2632 let config = MD032Config {
2633 allow_lazy_continuation: false,
2634 };
2635 let fixed = fix_with_config(content, config);
2636 assert!(fixed.contains("<>&"), "Should preserve special chars");
2637 assert!(fixed.contains("\"quotes\""), "Should preserve quotes");
2638 assert_eq!(fixed, "- Item with special chars: <>&\n Continuation with: \"quotes\"");
2640 }
2641
2642 #[test]
2643 fn test_lazy_continuation_fix_idempotent() {
2644 let content = "- Item\nLazy";
2646 let config = MD032Config {
2647 allow_lazy_continuation: false,
2648 };
2649 let fixed_once = fix_with_config(content, config.clone());
2650 let fixed_twice = fix_with_config(&fixed_once, config);
2651 assert_eq!(fixed_once, fixed_twice, "Fix should be idempotent");
2652 }
2653
2654 #[test]
2655 fn test_lazy_continuation_config_default_allows() {
2656 let content = "- Item\nLazy text that continues";
2658 let default_config = MD032Config::default();
2659 assert!(
2660 default_config.allow_lazy_continuation,
2661 "Default should allow lazy continuation"
2662 );
2663 let warnings = lint_with_config(content, default_config);
2664 assert_eq!(warnings.len(), 0, "Default config should not warn on lazy continuation");
2665 }
2666
2667 #[test]
2668 fn test_lazy_continuation_after_multi_line_item() {
2669 let content = "- Item line 1\n Item line 2 (indented)\nLazy (not indented)";
2671 let config = MD032Config {
2672 allow_lazy_continuation: false,
2673 };
2674 let warnings = lint_with_config(content, config.clone());
2675 assert_eq!(
2676 warnings.len(),
2677 1,
2678 "Should warn only for the lazy line, not the indented line"
2679 );
2680 }
2681
2682 #[test]
2684 fn test_blockquote_list_with_continuation_and_nested() {
2685 let content = "> - item 1\n> continuation\n> - nested\n> - item 2";
2688 let warnings = lint(content);
2689 assert_eq!(
2690 warnings.len(),
2691 0,
2692 "Blockquoted list with continuation and nested items should have no warnings. Got: {warnings:?}"
2693 );
2694 }
2695
2696 #[test]
2697 fn test_blockquote_list_simple() {
2698 let content = "> - item 1\n> - item 2";
2700 let warnings = lint(content);
2701 assert_eq!(warnings.len(), 0, "Simple blockquoted list should have no warnings");
2702 }
2703
2704 #[test]
2705 fn test_blockquote_list_with_continuation_only() {
2706 let content = "> - item 1\n> continuation\n> - item 2";
2708 let warnings = lint(content);
2709 assert_eq!(
2710 warnings.len(),
2711 0,
2712 "Blockquoted list with continuation should have no warnings"
2713 );
2714 }
2715
2716 #[test]
2717 fn test_blockquote_list_with_lazy_continuation() {
2718 let content = "> - item 1\n> lazy continuation\n> - item 2";
2720 let warnings = lint(content);
2721 assert_eq!(
2722 warnings.len(),
2723 0,
2724 "Blockquoted list with lazy continuation should have no warnings"
2725 );
2726 }
2727
2728 #[test]
2729 fn test_nested_blockquote_list() {
2730 let content = ">> - item 1\n>> continuation\n>> - nested\n>> - item 2";
2732 let warnings = lint(content);
2733 assert_eq!(warnings.len(), 0, "Nested blockquote list should have no warnings");
2734 }
2735
2736 #[test]
2737 fn test_blockquote_list_needs_preceding_blank() {
2738 let content = "> Text before\n> - item 1\n> - item 2";
2740 let warnings = lint(content);
2741 assert_eq!(
2742 warnings.len(),
2743 1,
2744 "Should warn for missing blank before blockquoted list"
2745 );
2746 }
2747
2748 #[test]
2749 fn test_blockquote_list_properly_separated() {
2750 let content = "> Text before\n>\n> - item 1\n> - item 2\n>\n> Text after";
2752 let warnings = lint(content);
2753 assert_eq!(
2754 warnings.len(),
2755 0,
2756 "Properly separated blockquoted list should have no warnings"
2757 );
2758 }
2759
2760 #[test]
2761 fn test_blockquote_ordered_list() {
2762 let content = "> 1. item 1\n> continuation\n> 2. item 2";
2764 let warnings = lint(content);
2765 assert_eq!(warnings.len(), 0, "Ordered list in blockquote should have no warnings");
2766 }
2767
2768 #[test]
2769 fn test_blockquote_list_with_empty_blockquote_line() {
2770 let content = "> - item 1\n>\n> - item 2";
2772 let warnings = lint(content);
2773 assert_eq!(warnings.len(), 0, "Empty blockquote line should not break list");
2774 }
2775
2776 #[test]
2778 fn test_blockquote_list_multi_paragraph_items() {
2779 let content = "# Test\n\n> Some intro text\n> \n> * List item 1\n> \n> Continuation\n> * List item 2\n";
2782 let warnings = lint(content);
2783 assert_eq!(
2784 warnings.len(),
2785 0,
2786 "Multi-paragraph list items in blockquotes should have no warnings. Got: {warnings:?}"
2787 );
2788 }
2789
2790 #[test]
2792 fn test_blockquote_ordered_list_multi_paragraph_items() {
2793 let content = "> 1. First item\n> \n> Continuation of first\n> 2. Second item\n";
2794 let warnings = lint(content);
2795 assert_eq!(
2796 warnings.len(),
2797 0,
2798 "Ordered multi-paragraph list items in blockquotes should have no warnings. Got: {warnings:?}"
2799 );
2800 }
2801
2802 #[test]
2804 fn test_blockquote_list_multiple_continuations() {
2805 let content = "> - Item 1\n> \n> First continuation\n> \n> Second continuation\n> - Item 2\n";
2806 let warnings = lint(content);
2807 assert_eq!(
2808 warnings.len(),
2809 0,
2810 "Multiple continuation paragraphs should not break blockquote list. Got: {warnings:?}"
2811 );
2812 }
2813
2814 #[test]
2816 fn test_nested_blockquote_multi_paragraph_list() {
2817 let content = ">> - Item 1\n>> \n>> Continuation\n>> - Item 2\n";
2818 let warnings = lint(content);
2819 assert_eq!(
2820 warnings.len(),
2821 0,
2822 "Nested blockquote multi-paragraph list should have no warnings. Got: {warnings:?}"
2823 );
2824 }
2825
2826 #[test]
2828 fn test_triple_nested_blockquote_multi_paragraph_list() {
2829 let content = ">>> - Item 1\n>>> \n>>> Continuation\n>>> - Item 2\n";
2830 let warnings = lint(content);
2831 assert_eq!(
2832 warnings.len(),
2833 0,
2834 "Triple-nested blockquote multi-paragraph list should have no warnings. Got: {warnings:?}"
2835 );
2836 }
2837
2838 #[test]
2840 fn test_blockquote_list_last_item_continuation() {
2841 let content = "> - Item 1\n> - Item 2\n> \n> Continuation of item 2\n";
2842 let warnings = lint(content);
2843 assert_eq!(
2844 warnings.len(),
2845 0,
2846 "Last item with continuation should have no warnings. Got: {warnings:?}"
2847 );
2848 }
2849
2850 #[test]
2852 fn test_blockquote_list_first_item_only_continuation() {
2853 let content = "> - Item 1\n> \n> Continuation of item 1\n";
2854 let warnings = lint(content);
2855 assert_eq!(
2856 warnings.len(),
2857 0,
2858 "Single item with continuation should have no warnings. Got: {warnings:?}"
2859 );
2860 }
2861
2862 #[test]
2866 fn test_blockquote_level_change_breaks_list() {
2867 let content = "> - Item in single blockquote\n>> - Item in nested blockquote\n";
2869 let warnings = lint(content);
2870 assert!(
2874 warnings.len() <= 2,
2875 "Blockquote level change warnings should be reasonable. Got: {warnings:?}"
2876 );
2877 }
2878
2879 #[test]
2881 fn test_exit_blockquote_needs_blank_before_list() {
2882 let content = "> Blockquote text\n\n- List outside blockquote\n";
2884 let warnings = lint(content);
2885 assert_eq!(
2886 warnings.len(),
2887 0,
2888 "List after blank line outside blockquote should be fine. Got: {warnings:?}"
2889 );
2890
2891 let content2 = "> Blockquote text\n- List outside blockquote\n";
2895 let warnings2 = lint(content2);
2896 assert!(
2898 warnings2.len() <= 1,
2899 "List after blockquote warnings should be reasonable. Got: {warnings2:?}"
2900 );
2901 }
2902
2903 #[test]
2905 fn test_blockquote_multi_paragraph_all_unordered_markers() {
2906 let content_dash = "> - Item 1\n> \n> Continuation\n> - Item 2\n";
2908 let warnings = lint(content_dash);
2909 assert_eq!(warnings.len(), 0, "Dash marker should work. Got: {warnings:?}");
2910
2911 let content_asterisk = "> * Item 1\n> \n> Continuation\n> * Item 2\n";
2913 let warnings = lint(content_asterisk);
2914 assert_eq!(warnings.len(), 0, "Asterisk marker should work. Got: {warnings:?}");
2915
2916 let content_plus = "> + Item 1\n> \n> Continuation\n> + Item 2\n";
2918 let warnings = lint(content_plus);
2919 assert_eq!(warnings.len(), 0, "Plus marker should work. Got: {warnings:?}");
2920 }
2921
2922 #[test]
2924 fn test_blockquote_multi_paragraph_parenthesis_marker() {
2925 let content = "> 1) Item 1\n> \n> Continuation\n> 2) Item 2\n";
2926 let warnings = lint(content);
2927 assert_eq!(
2928 warnings.len(),
2929 0,
2930 "Parenthesis ordered markers should work. Got: {warnings:?}"
2931 );
2932 }
2933
2934 #[test]
2936 fn test_blockquote_multi_paragraph_multi_digit_numbers() {
2937 let content = "> 10. Item 10\n> \n> Continuation of item 10\n> 11. Item 11\n";
2939 let warnings = lint(content);
2940 assert_eq!(
2941 warnings.len(),
2942 0,
2943 "Multi-digit ordered list should work. Got: {warnings:?}"
2944 );
2945 }
2946
2947 #[test]
2949 fn test_blockquote_multi_paragraph_with_formatting() {
2950 let content = "> - Item with **bold**\n> \n> Continuation with *emphasis* and `code`\n> - Item 2\n";
2951 let warnings = lint(content);
2952 assert_eq!(
2953 warnings.len(),
2954 0,
2955 "Continuation with inline formatting should work. Got: {warnings:?}"
2956 );
2957 }
2958
2959 #[test]
2961 fn test_blockquote_multi_paragraph_all_items_have_continuation() {
2962 let content = "> - Item 1\n> \n> Continuation 1\n> - Item 2\n> \n> Continuation 2\n> - Item 3\n> \n> Continuation 3\n";
2963 let warnings = lint(content);
2964 assert_eq!(
2965 warnings.len(),
2966 0,
2967 "All items with continuations should work. Got: {warnings:?}"
2968 );
2969 }
2970
2971 #[test]
2973 fn test_blockquote_multi_paragraph_lowercase_continuation() {
2974 let content = "> - Item 1\n> \n> and this continues the item\n> - Item 2\n";
2975 let warnings = lint(content);
2976 assert_eq!(
2977 warnings.len(),
2978 0,
2979 "Lowercase continuation should work. Got: {warnings:?}"
2980 );
2981 }
2982
2983 #[test]
2985 fn test_blockquote_multi_paragraph_uppercase_continuation() {
2986 let content = "> - Item 1\n> \n> This continues the item with uppercase\n> - Item 2\n";
2987 let warnings = lint(content);
2988 assert_eq!(
2989 warnings.len(),
2990 0,
2991 "Uppercase continuation with proper indent should work. Got: {warnings:?}"
2992 );
2993 }
2994
2995 #[test]
2997 fn test_blockquote_separate_ordered_unordered_multi_paragraph() {
2998 let content = "> - Unordered item\n> \n> Continuation\n> \n> 1. Ordered item\n> \n> Continuation\n";
3000 let warnings = lint(content);
3001 assert!(
3003 warnings.len() <= 1,
3004 "Separate lists with continuations should be reasonable. Got: {warnings:?}"
3005 );
3006 }
3007
3008 #[test]
3010 fn test_blockquote_multi_paragraph_bare_marker_blank() {
3011 let content = "> - Item 1\n>\n> Continuation\n> - Item 2\n";
3013 let warnings = lint(content);
3014 assert_eq!(warnings.len(), 0, "Bare > as blank line should work. Got: {warnings:?}");
3015 }
3016
3017 #[test]
3018 fn test_blockquote_list_varying_spaces_after_marker() {
3019 let content = "> - item 1\n> continuation with more indent\n> - item 2";
3021 let warnings = lint(content);
3022 assert_eq!(warnings.len(), 0, "Varying spaces after > should not break list");
3023 }
3024
3025 #[test]
3026 fn test_deeply_nested_blockquote_list() {
3027 let content = ">>> - item 1\n>>> continuation\n>>> - item 2";
3029 let warnings = lint(content);
3030 assert_eq!(
3031 warnings.len(),
3032 0,
3033 "Deeply nested blockquote list should have no warnings"
3034 );
3035 }
3036
3037 #[test]
3038 fn test_blockquote_level_change_in_list() {
3039 let content = "> - item 1\n>> - deeper item\n> - item 2";
3041 let warnings = lint(content);
3044 assert!(
3045 !warnings.is_empty(),
3046 "Blockquote level change should break list and trigger warnings"
3047 );
3048 }
3049
3050 #[test]
3051 fn test_blockquote_list_with_code_span() {
3052 let content = "> - item with `code`\n> continuation\n> - item 2";
3054 let warnings = lint(content);
3055 assert_eq!(
3056 warnings.len(),
3057 0,
3058 "Blockquote list with code span should have no warnings"
3059 );
3060 }
3061
3062 #[test]
3063 fn test_code_span_html_comment_delimiters_no_false_positive() {
3064 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";
3070 let warnings = lint(content);
3071 assert_eq!(
3072 warnings.len(),
3073 0,
3074 "code-span HTML comment delimiters must not cause MD032 false positives, got: {warnings:?}"
3075 );
3076 }
3077
3078 #[test]
3079 fn test_code_span_html_comment_delimiters_fix_is_idempotent() {
3080 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";
3085 let fixed = fix(content);
3086 assert_eq!(
3087 fixed, content,
3088 "MD032 fix must be a no-op for content whose only `<!--`/`-->` are inside code spans"
3089 );
3090 }
3091
3092 #[test]
3093 fn test_blockquote_list_at_document_end() {
3094 let content = "> Some text\n>\n> - item 1\n> - item 2";
3096 let warnings = lint(content);
3097 assert_eq!(
3098 warnings.len(),
3099 0,
3100 "Blockquote list at document end should have no warnings"
3101 );
3102 }
3103
3104 #[test]
3105 fn test_fix_preserves_blockquote_prefix_before_list() {
3106 let content = "> Text before
3108> - Item 1
3109> - Item 2";
3110 let fixed = fix(content);
3111
3112 let expected = "> Text before
3114>
3115> - Item 1
3116> - Item 2";
3117 assert_eq!(
3118 fixed, expected,
3119 "Fix should insert '>' blank line, not plain blank line"
3120 );
3121 }
3122
3123 #[test]
3124 fn test_fix_preserves_triple_nested_blockquote_prefix_for_list() {
3125 let content = ">>> Triple nested
3128>>> - Item 1
3129>>> - Item 2
3130>>> More text";
3131 let fixed = fix(content);
3132
3133 let expected = ">>> Triple nested
3135>>>
3136>>> - Item 1
3137>>> - Item 2
3138>>> More text";
3139 assert_eq!(
3140 fixed, expected,
3141 "Fix should preserve triple-nested blockquote prefix '>>>'"
3142 );
3143 }
3144
3145 fn lint_quarto(content: &str) -> Vec<LintWarning> {
3148 let rule = MD032BlanksAroundLists::default();
3149 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
3150 rule.check(&ctx).unwrap()
3151 }
3152
3153 #[test]
3154 fn test_quarto_list_after_div_open() {
3155 let content = "Content\n\n::: {.callout-note}\n- Item 1\n- Item 2\n:::\n";
3157 let warnings = lint_quarto(content);
3158 assert!(
3160 warnings.is_empty(),
3161 "Quarto div marker should be transparent before list: {warnings:?}"
3162 );
3163 }
3164
3165 #[test]
3166 fn test_quarto_list_before_div_close() {
3167 let content = "::: {.callout-note}\n\n- Item 1\n- Item 2\n:::\n";
3169 let warnings = lint_quarto(content);
3170 assert!(
3172 warnings.is_empty(),
3173 "Quarto div marker should be transparent after list: {warnings:?}"
3174 );
3175 }
3176
3177 #[test]
3178 fn test_quarto_list_needs_blank_without_div() {
3179 let content = "Content\n::: {.callout-note}\n- Item 1\n- Item 2\n:::\n";
3181 let warnings = lint_quarto(content);
3182 assert!(
3185 !warnings.is_empty(),
3186 "Should still require blank when not present: {warnings:?}"
3187 );
3188 }
3189
3190 #[test]
3191 fn test_quarto_list_in_callout_with_content() {
3192 let content = "::: {.callout-note}\nNote introduction:\n\n- Item 1\n- Item 2\n\nMore note content.\n:::\n";
3194 let warnings = lint_quarto(content);
3195 assert!(
3196 warnings.is_empty(),
3197 "List with proper blanks inside callout should pass: {warnings:?}"
3198 );
3199 }
3200
3201 #[test]
3202 fn test_quarto_div_markers_not_transparent_in_standard_flavor() {
3203 let content = "Content\n\n:::\n- Item 1\n- Item 2\n:::\n";
3205 let warnings = lint(content); assert!(
3208 !warnings.is_empty(),
3209 "Standard flavor should not treat ::: as transparent: {warnings:?}"
3210 );
3211 }
3212
3213 #[test]
3214 fn test_quarto_nested_divs_with_list() {
3215 let content = "::: {.outer}\n::: {.inner}\n\n- Item 1\n- Item 2\n\n:::\n:::\n";
3217 let warnings = lint_quarto(content);
3218 assert!(warnings.is_empty(), "Nested divs with list should work: {warnings:?}");
3219 }
3220
3221 #[test]
3222 fn test_issue512_complex_nested_list_with_continuation() {
3223 let content = "\
3226- First level of indentation.
3227 - Second level of indentation.
3228 - Third level of indentation.
3229 - Third level of indentation.
3230
3231 Second level list continuation.
3232
3233 First level list continuation.
3234- First level of indentation.
3235";
3236 let warnings = lint(content);
3237 assert!(
3238 warnings.is_empty(),
3239 "Nested list with parent-level continuation should produce no warnings. Got: {warnings:?}"
3240 );
3241 }
3242
3243 #[test]
3244 fn test_issue512_continuation_at_root_level() {
3245 let content = "\
3249- First level.
3250 - Second level.
3251
3252 First level continuation.
3253
3254Root level lazy continuation.
3255- Another first level item.
3256";
3257 let warnings = lint(content);
3258 assert_eq!(
3259 warnings.len(),
3260 1,
3261 "Should warn on line 7 (new list after break). Got: {warnings:?}"
3262 );
3263 assert_eq!(warnings[0].line, 7);
3264 }
3265
3266 #[test]
3267 fn test_issue512_three_level_nesting_continuation_at_each_level() {
3268 let content = "\
3270- Level 1 item.
3271 - Level 2 item.
3272 - Level 3 item.
3273
3274 Level 3 continuation.
3275
3276 Level 2 continuation.
3277
3278 Level 1 continuation (indented under marker).
3279- Another level 1 item.
3280";
3281 let warnings = lint(content);
3282 assert!(
3283 warnings.is_empty(),
3284 "Continuation at each nesting level should produce no warnings. Got: {warnings:?}"
3285 );
3286 }
3287
3288 #[test]
3289 fn test_pandoc_list_after_div_open() {
3290 let rule = MD032BlanksAroundLists::default();
3293 let content = "Content\n\n::: {.callout-note}\n- Item 1\n- Item 2\n:::\n";
3294 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
3295 let warnings = rule.check(&ctx).unwrap();
3296 assert!(
3297 warnings.is_empty(),
3298 "MD032 should treat Pandoc div marker as transparent before list: {warnings:?}"
3299 );
3300 }
3301
3302 #[test]
3303 fn test_md032_html_comment() {
3304 let rule = MD032BlanksAroundLists::default();
3305 let content = "text\n<!--\n- Item 1\n- Item 2\n-->\ntext";
3306 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3307 let warnings = rule.check(&ctx).unwrap();
3308 assert!(
3309 warnings.is_empty(),
3310 "MD032 should not require blank lines around lists inside HTML comments: {warnings:?}"
3311 );
3312 }
3313
3314 #[test]
3315 fn test_mkdocs_admonition_nested_ordered_list_not_flagged() {
3316 let rule = MD032BlanksAroundLists::default();
3322 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";
3323 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3324 let warnings = rule.check(&ctx).unwrap();
3325 assert!(
3326 warnings.is_empty(),
3327 "admonition-nested ordered list should not be flagged: {warnings:?}"
3328 );
3329 }
3330
3331 #[test]
3332 fn test_mkdocs_admonition_nested_ordered_list_cascade_not_flagged() {
3333 let rule = MD032BlanksAroundLists::default();
3337 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";
3338 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3339 let warnings = rule.check(&ctx).unwrap();
3340 assert!(
3341 warnings.is_empty(),
3342 "cascading admonition-nested ordered list should not be flagged: {warnings:?}"
3343 );
3344 }
3345
3346 #[test]
3347 fn test_mkdocs_content_tab_nested_ordered_list_not_flagged() {
3348 let rule = MD032BlanksAroundLists::default();
3350 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";
3351 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3352 let warnings = rule.check(&ctx).unwrap();
3353 assert!(
3354 warnings.is_empty(),
3355 "content-tab-nested ordered list should not be flagged: {warnings:?}"
3356 );
3357 }
3358
3359 #[test]
3360 fn test_mkdocs_admonition_prose_then_non1_item_still_flagged() {
3361 let rule = MD032BlanksAroundLists::default();
3367 let content = "1. no error here\n\n!!! example\n\n Intro.\n 2. item\n";
3368 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3369 let warnings = rule.check(&ctx).unwrap();
3370 assert_eq!(
3371 warnings.len(),
3372 1,
3373 "prose then non-1 item inside an admonition must stay flagged: {warnings:?}"
3374 );
3375 }
3376
3377 #[test]
3378 fn test_mkdocs_admonition_prose_after_list_item_breaks_continuation() {
3379 let rule = MD032BlanksAroundLists::default();
3384 let content = "1. no error here\n\n!!! example\n\n 1. one.\n Intro prose.\n 2. two\n";
3385 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3386 let warnings = rule.check(&ctx).unwrap();
3387 assert_eq!(
3388 warnings.len(),
3389 1,
3390 "prose at item indent breaks the list continuation, item must stay flagged: {warnings:?}"
3391 );
3392 }
3393
3394 #[test]
3395 fn test_mkdocs_admonition_wrapped_item_continuation_not_flagged() {
3396 let rule = MD032BlanksAroundLists::default();
3401 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";
3402 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3403 let warnings = rule.check(&ctx).unwrap();
3404 assert!(
3405 warnings.is_empty(),
3406 "wrapped continuation of a nested list item must not be flagged: {warnings:?}"
3407 );
3408 }
3409
3410 #[test]
3411 fn test_mkdocs_ambiguous_prose_non1_ordered_item_still_flagged() {
3412 let rule = MD032BlanksAroundLists::default();
3418 let content = "1. no error here\n\nno error here.\n2. error here because previous line ends with a period.\n";
3419 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3420 let warnings = rule.check(&ctx).unwrap();
3421 assert_eq!(
3422 warnings.len(),
3423 1,
3424 "ambiguous non-1 ordered item outside any container should still be flagged: {warnings:?}"
3425 );
3426 assert_eq!(warnings[0].line, 4);
3427 assert!(warnings[0].message.contains("non-1"));
3428 }
3429
3430 #[test]
3431 fn test_mkdocs_admonition_nested_list_without_trailing_punctuation_not_flagged() {
3432 let rule = MD032BlanksAroundLists::default();
3438 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";
3439 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3440 let warnings = rule.check(&ctx).unwrap();
3441 assert!(
3442 warnings.is_empty(),
3443 "admonition-nested ordered list without trailing punctuation should not be flagged: {warnings:?}"
3444 );
3445 }
3446
3447 #[test]
3448 fn test_standard_flavor_admonition_indented_list_unchanged() {
3449 let rule = MD032BlanksAroundLists::default();
3454 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";
3455 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
3456 let warnings = rule.check(&ctx).unwrap();
3457 assert!(
3458 warnings.is_empty(),
3459 "indented code block under standard flavor should not be flagged: {warnings:?}"
3460 );
3461 }
3462
3463 #[test]
3464 fn test_mkdocs_html_markdown_div_nested_ordered_list_still_flagged() {
3465 let rule = MD032BlanksAroundLists::default();
3470 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";
3471 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
3472 let warnings = rule.check(&ctx).unwrap();
3473 assert_eq!(
3474 warnings.len(),
3475 1,
3476 "markdown=\"1\" div nested ordered list behavior must stay unchanged: {warnings:?}"
3477 );
3478 assert_eq!(warnings[0].line, 6);
3479 }
3480
3481 #[test]
3482 fn test_pseudo_list_marker_after_list() {
3483 let content = indoc::indoc! {"
3484 - Item 1
3485 Item 1 content.
3486
3487 The unsigned-integer types may be written `uN`, with `N` a positive multiple of
3488 8. Unsigned integer types wrap around on overflow; we strongly advise that they
3489 are not used except when those semantics are desired.
3490 "};
3491 let warnings = lint(content);
3492 assert!(
3493 warnings.is_empty(),
3494 "Expected no warnings for pseudo-list marker after list, but got: {warnings:?}"
3495 );
3496 }
3497
3498 #[test]
3499 fn test_pseudo_list_marker_without_preceding_list() {
3500 let content = indoc::indoc! {"
3501 The unsigned-integer types may be written `uN`, with `N` a positive multiple of
3502 8. Unsigned integer types wrap around on overflow; we strongly advise that they
3503 are not used except when those semantics are desired.
3504 "};
3505 let warnings = lint(content);
3506 assert!(
3507 warnings.is_empty(),
3508 "Expected no warnings for pseudo-list marker without preceding list, but got: {warnings:?}"
3509 );
3510 }
3511}