1use std::ops::ControlFlow;
7
8use crate::lint_context::{LineInfo, LintContext};
9use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
10
11#[derive(Clone, Default)]
24pub struct MD077ListContinuationIndent;
25
26impl MD077ListContinuationIndent {
27 const TASK_CHECKBOX_PREFIX_LEN: usize = 4;
30
31 fn is_task_list_item(line: &str, content_col: usize) -> bool {
47 line.as_bytes()
48 .get(content_col..content_col + Self::TASK_CHECKBOX_PREFIX_LEN)
49 .is_some_and(|window| matches!(window, b"[ ] " | b"[x] " | b"[X] "))
50 }
51
52 fn is_block_level_construct(trimmed: &str) -> bool {
54 if trimmed.starts_with("[^") && trimmed.contains("]:") {
56 return true;
57 }
58 if trimmed.starts_with("*[") && trimmed.contains("]:") {
60 return true;
61 }
62 if trimmed.starts_with('[') && !trimmed.starts_with("[^") && trimmed.contains("]: ") {
65 return true;
66 }
67 false
68 }
69
70 fn is_code_fence(trimmed: &str) -> bool {
72 let bytes = trimmed.as_bytes();
73 if bytes.len() < 3 {
74 return false;
75 }
76 let ch = bytes[0];
77 (ch == b'`' || ch == b'~') && bytes[1] == ch && bytes[2] == ch
78 }
79
80 fn starts_with_list_marker(trimmed: &str) -> bool {
84 let bytes = trimmed.as_bytes();
85 match bytes.first() {
86 Some(b'*' | b'-' | b'+') => bytes.get(1).is_some_and(|&b| b == b' ' || b == b'\t'),
87 Some(b'0'..=b'9') => {
88 let rest = trimmed.trim_start_matches(|c: char| c.is_ascii_digit());
89 rest.starts_with(". ") || rest.starts_with(") ")
90 }
91 _ => false,
92 }
93 }
94
95 fn find_fence_closer(ctx: &LintContext, opener_line: usize) -> usize {
99 let mut closer_line = opener_line;
100 for peek in (opener_line + 1)..=ctx.lines.len() {
101 let Some(peek_info) = ctx.line_info(peek) else { break };
102 if peek_info.in_code_block {
103 closer_line = peek;
104 } else {
105 break;
106 }
107 }
108 closer_line
109 }
110
111 fn build_compound_fence_fix(
146 ctx: &LintContext,
147 opener_line: usize,
148 closer_line: usize,
149 opener_actual: usize,
150 required: usize,
151 ) -> Option<Fix> {
152 if required <= opener_actual {
153 return None;
154 }
155 let opener_info = ctx.line_info(opener_line)?;
156 let closer_info = ctx.line_info(closer_line)?;
157
158 let fix_start = opener_info.byte_offset;
159 let fix_end = closer_info.byte_offset + closer_info.byte_len;
160
161 let mut replacement = String::new();
162 for i in opener_line..=closer_line {
163 let info = ctx.line_info(i)?;
164 if i > opener_line {
165 replacement.push('\n');
166 }
167 let line = info.content(ctx.content);
168 if info.is_blank {
169 replacement.push_str(line);
171 } else {
172 let new_visual = if i == opener_line || i == closer_line {
173 required
174 } else {
175 info.visual_indent.max(required)
176 };
177 for _ in 0..new_visual {
178 replacement.push(' ');
179 }
180 replacement.push_str(&line[info.indent..]);
181 }
182 }
183
184 Some(Fix::new(fix_start..fix_end, replacement))
185 }
186
187 fn walk_item_continuation<F>(
210 ctx: &LintContext,
211 item_line: usize,
212 range_end: usize,
213 marker_col: usize,
214 mut per_line: F,
215 ) where
216 F: FnMut(&ContinuationLine<'_>) -> ControlFlow<()>,
217 {
218 let mut saw_blank = false;
219 let mut nested_content_col: Option<usize> = None;
220
221 for line_num in (item_line + 1)..=range_end {
222 let Some(info) = ctx.line_info(line_num) else {
223 continue;
224 };
225
226 let trimmed = info.content(ctx.content).trim_start();
227
228 if Self::should_skip_line(info, trimmed) {
229 continue;
230 }
231
232 if info.is_blank {
233 saw_blank = true;
234 continue;
235 }
236
237 if let Some(ref li) = info.list_item {
238 nested_content_col = (li.marker_column > marker_col).then_some(li.content_column);
239 saw_blank = false;
240 continue;
241 }
242
243 if info.heading.is_some() || info.is_horizontal_rule {
244 break;
245 }
246
247 if Self::is_block_level_construct(trimmed) {
248 continue;
249 }
250
251 let col = info.visual_indent;
252
253 if let Some(ncc) = nested_content_col {
254 if col >= ncc {
255 continue;
256 }
257 nested_content_col = None;
258 }
259
260 if saw_blank && col <= marker_col {
261 break;
262 }
263
264 let line = ContinuationLine {
265 line_num,
266 info,
267 trimmed,
268 actual: col,
269 saw_blank,
270 };
271 if per_line(&line).is_break() {
272 break;
273 }
274 }
275 }
276
277 fn sibling_column_usage(
287 ctx: &LintContext,
288 item_line: usize,
289 range_end: usize,
290 marker_col: usize,
291 content_col: usize,
292 task_col: usize,
293 ) -> (bool, bool) {
294 let mut uses_content = false;
295 let mut uses_task = false;
296
297 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
298 if line.actual == content_col {
299 uses_content = true;
300 }
301 if line.actual == task_col {
302 uses_task = true;
303 }
304 if uses_content && uses_task {
305 ControlFlow::Break(())
306 } else {
307 ControlFlow::Continue(())
308 }
309 });
310
311 (uses_content, uses_task)
312 }
313
314 fn compute_fix_target(
320 actual: usize,
321 required: usize,
322 task_col: Option<usize>,
323 uses_content_col: bool,
324 uses_task_col: bool,
325 ) -> usize {
326 let Some(t) = task_col else { return required };
327 match actual.abs_diff(t).cmp(&actual.abs_diff(required)) {
328 std::cmp::Ordering::Less => t,
329 std::cmp::Ordering::Greater => required,
330 std::cmp::Ordering::Equal => match (uses_task_col, uses_content_col) {
331 (true, false) => t,
332 _ => required,
333 },
334 }
335 }
336
337 fn should_skip_line(info: &crate::lint_context::LineInfo, trimmed: &str) -> bool {
342 if info.in_code_block && !Self::is_code_fence(trimmed) {
343 return true;
344 }
345 info.in_front_matter
346 || info.in_html_block
347 || info.in_html_comment
348 || info.in_mdx_comment
349 || info.in_mkdocstrings
350 || info.in_esm_block
351 || info.in_math_block
352 || info.in_admonition
353 || info.in_content_tab
354 || info.in_pymdown_block
355 || info.in_definition_list
356 || info.in_mkdocs_html_markdown
357 || info.in_kramdown_extension_block
358 }
359
360 fn build_over_indent_warning(
369 ctx: &LintContext,
370 line: &ContinuationLine<'_>,
371 fix_target: usize,
372 message: String,
373 ) -> LintWarning {
374 let line_content = line.info.content(ctx.content);
375 let fix_start = line.info.byte_offset;
376 let fix_end = fix_start + line.info.indent;
377 LintWarning {
378 rule_name: Some("MD077".to_string()),
379 line: line.line_num,
380 column: 1,
381 end_line: line.line_num,
382 end_column: line_content.len() + 1,
383 message,
384 severity: Severity::Warning,
385 fix: Some(Fix::new(fix_start..fix_end, " ".repeat(fix_target))),
386 }
387 }
388
389 fn build_under_indent_warning(
401 ctx: &LintContext,
402 line: &ContinuationLine<'_>,
403 required: usize,
404 message: String,
405 ) -> UnderIndentOutcome {
406 let line_content = line.info.content(ctx.content);
407 let is_fence_opener = line.info.in_code_block
408 && Self::is_code_fence(line.trimmed)
409 && ctx.line_info(line.line_num - 1).is_none_or(|p| !p.in_code_block);
410
411 let (fix, warn_end_line, warn_end_column, compound_closer) = if is_fence_opener {
412 let closer_line = Self::find_fence_closer(ctx, line.line_num);
413 let fix = Self::build_compound_fence_fix(ctx, line.line_num, closer_line, line.actual, required);
414 let end_column = ctx
415 .line_info(closer_line)
416 .map_or(line_content.len() + 1, |ci| ci.content(ctx.content).len() + 1);
417 let extra_flag = (closer_line != line.line_num).then_some(closer_line);
418 (fix, closer_line, end_column, extra_flag)
419 } else {
420 let fix_start = line.info.byte_offset;
421 let fix_end = fix_start + line.info.indent;
422 let fix = Some(Fix::new(fix_start..fix_end, " ".repeat(required)));
423 (fix, line.line_num, line_content.len() + 1, None)
424 };
425
426 UnderIndentOutcome {
427 warning: LintWarning {
428 rule_name: Some("MD077".to_string()),
429 line: line.line_num,
430 column: 1,
431 end_line: warn_end_line,
432 end_column: warn_end_column,
433 message,
434 severity: Severity::Warning,
435 fix,
436 },
437 also_flag_line: compound_closer,
438 }
439 }
440}
441
442struct ContinuationLine<'a> {
446 line_num: usize,
447 info: &'a LineInfo,
448 trimmed: &'a str,
449 actual: usize,
450 saw_blank: bool,
451}
452
453struct UnderIndentOutcome {
458 warning: LintWarning,
459 also_flag_line: Option<usize>,
460}
461
462impl Rule for MD077ListContinuationIndent {
463 fn name(&self) -> &'static str {
464 "MD077"
465 }
466
467 fn description(&self) -> &'static str {
468 "List continuation content indentation"
469 }
470
471 fn check(&self, ctx: &LintContext) -> LintResult {
472 if ctx.content.is_empty() {
473 return Ok(Vec::new());
474 }
475
476 let strict_indent = ctx.flavor.requires_strict_list_indent();
477 let total_lines = ctx.lines.len();
478 let mut warnings = Vec::new();
479 let mut flagged_lines = std::collections::HashSet::new();
480
481 let mut items: Vec<(usize, usize, usize, Option<usize>)> = Vec::new();
490 for block in &ctx.list_blocks {
491 for &item_line in &block.item_lines {
492 if let Some(info) = ctx.line_info(item_line)
493 && let Some(ref li) = info.list_item
494 {
495 let line = info.content(ctx.content);
496 let task_col = Self::is_task_list_item(line, li.content_column)
497 .then_some(li.content_column + Self::TASK_CHECKBOX_PREFIX_LEN);
498 items.push((item_line, li.marker_column, li.content_column, task_col));
499 }
500 }
501 }
502 items.sort_unstable();
503 items.dedup_by_key(|&mut (ln, _, _, _)| ln);
504
505 let scoped: Vec<(usize, usize, usize, Option<usize>, usize, usize)> = items
509 .iter()
510 .enumerate()
511 .map(|(item_idx, &(item_line, marker_col, content_col, task_col))| {
512 let required = if strict_indent { content_col.max(4) } else { content_col };
513 let range_end = items
514 .iter()
515 .skip(item_idx + 1)
516 .find(|&&(_, mc, _, _)| mc <= marker_col)
517 .map_or(total_lines, |&(ln, _, _, _)| ln - 1);
518 (item_line, marker_col, content_col, task_col, required, range_end)
519 })
520 .collect();
521
522 for &(item_line, marker_col, _content_col, _task_col, required, range_end) in &scoped {
530 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
531 let actual = line.actual;
532 if line.saw_blank && actual < required && flagged_lines.insert(line.line_num) {
533 let message = if strict_indent {
534 format!(
535 "Content inside list item needs {required} spaces of indentation \
536 for MkDocs compatibility (found {actual})",
537 )
538 } else {
539 format!(
540 "Content after blank line in list item needs {required} spaces of \
541 indentation to remain part of the list (found {actual})",
542 )
543 };
544 let outcome = Self::build_under_indent_warning(ctx, line, required, message);
545 if let Some(closer_line) = outcome.also_flag_line {
546 flagged_lines.insert(closer_line);
547 }
548 warnings.push(outcome.warning);
549 }
550 ControlFlow::Continue(())
551 });
552 }
553
554 for &(item_line, marker_col, content_col, task_col, required, range_end) in &scoped {
563 let (uses_content_col, uses_task_col) = match task_col {
567 Some(t) => Self::sibling_column_usage(ctx, item_line, range_end, marker_col, content_col, t),
568 None => (false, false),
569 };
570
571 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
572 let actual = line.actual;
573 if actual > required
574 && !line.info.in_code_block
575 && Some(actual) != task_col
576 && !Self::starts_with_list_marker(line.trimmed)
577 && flagged_lines.insert(line.line_num)
578 {
579 let fix_target =
580 Self::compute_fix_target(actual, required, task_col, uses_content_col, uses_task_col);
581 let message = match task_col {
582 Some(t) => format!(
583 "Continuation line over-indented \
584 (expected {required} or {t}, found {actual})"
585 ),
586 None => {
587 format!("Continuation line over-indented (expected {required}, found {actual})")
588 }
589 };
590 warnings.push(Self::build_over_indent_warning(ctx, line, fix_target, message));
591 }
592 ControlFlow::Continue(())
593 });
594 }
595
596 warnings.sort_by_key(|w| (w.line, w.column));
599
600 Ok(warnings)
601 }
602
603 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
604 let warnings = self.check(ctx)?;
605 let warnings =
606 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
607 if warnings.is_empty() {
608 return Ok(ctx.content.to_string());
609 }
610
611 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
613 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
614
615 let mut content = ctx.content.to_string();
616 for fix in fixes {
617 if fix.range.start <= content.len() && fix.range.end <= content.len() {
618 content.replace_range(fix.range, &fix.replacement);
619 }
620 }
621
622 Ok(content)
623 }
624
625 fn category(&self) -> RuleCategory {
626 RuleCategory::List
627 }
628
629 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
630 ctx.content.is_empty() || ctx.list_blocks.is_empty()
631 }
632
633 fn as_any(&self) -> &dyn std::any::Any {
634 self
635 }
636
637 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
638 where
639 Self: Sized,
640 {
641 Box::new(Self)
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648 use crate::config::MarkdownFlavor;
649
650 fn check(content: &str) -> Vec<LintWarning> {
651 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
652 let rule = MD077ListContinuationIndent;
653 rule.check(&ctx).unwrap()
654 }
655
656 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
657 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
658 let rule = MD077ListContinuationIndent;
659 rule.check(&ctx).unwrap()
660 }
661
662 fn fix(content: &str) -> String {
663 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
664 let rule = MD077ListContinuationIndent;
665 rule.fix(&ctx).unwrap()
666 }
667
668 fn fix_mkdocs(content: &str) -> String {
669 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
670 let rule = MD077ListContinuationIndent;
671 rule.fix(&ctx).unwrap()
672 }
673
674 #[test]
677 fn tight_lazy_continuation_zero_indent_not_flagged() {
678 let content = "- Item\ncontinuation\n";
680 assert!(check(content).is_empty());
681 }
682
683 #[test]
684 fn tight_continuation_correct_indent_not_flagged() {
685 let content = "1. Item\n continuation\n";
687 assert!(check(content).is_empty());
688 }
689
690 #[test]
691 fn tight_continuation_over_indented_ordered() {
692 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
694 let warnings = check(content);
695 assert_eq!(warnings.len(), 1);
696 assert_eq!(warnings[0].line, 2);
697 assert!(warnings[0].message.contains("over-indented"));
698 }
699
700 #[test]
701 fn tight_continuation_over_indented_unordered() {
702 let content = "- Item\n over-indented\n";
704 let warnings = check(content);
705 assert_eq!(warnings.len(), 1);
706 assert_eq!(warnings[0].line, 2);
707 }
708
709 #[test]
710 fn tight_continuation_multiple_over_indented_lines() {
711 let content = "1. Item\n line one\n line two\n line three\n";
712 let warnings = check(content);
713 assert_eq!(warnings.len(), 3);
714 }
715
716 #[test]
717 fn tight_continuation_mixed_correct_and_over() {
718 let content = "1. Item\n correct\n over-indented\n correct again\n";
719 let warnings = check(content);
720 assert_eq!(warnings.len(), 1);
721 assert_eq!(warnings[0].line, 3);
722 }
723
724 #[test]
725 fn tight_continuation_nested_over_indented() {
726 let content = "- L1\n - L2\n over-indented continuation of L2\n";
728 let warnings = check(content);
729 assert_eq!(warnings.len(), 1);
730 assert_eq!(warnings[0].line, 3);
731 assert!(warnings[0].message.contains("expected 4"));
733 assert!(warnings[0].message.contains("found 5"));
734 }
735
736 #[test]
737 fn tight_continuation_nested_correct_indent_not_flagged() {
738 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
741 assert!(check(content).is_empty());
742 }
743
744 #[test]
745 fn fix_tight_continuation_nested_over_indented() {
746 let content = "- L1\n - L2\n over-indented continuation of L2\n";
748 let fixed = fix(content);
749 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
750 }
751
752 #[test]
753 fn tight_continuation_under_indented_not_flagged() {
754 let content = "1. Item\n under-indented\n";
757 assert!(check(content).is_empty());
758 }
759
760 #[test]
761 fn tight_continuation_tab_over_indented() {
762 let content = "- Item\n\tover-indented\n";
764 let warnings = check(content);
765 assert_eq!(warnings.len(), 1);
766 }
767
768 #[test]
769 fn fix_tight_continuation_over_indented_ordered() {
770 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
771 let fixed = fix(content);
772 assert_eq!(
773 fixed,
774 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
775 );
776 }
777
778 #[test]
779 fn fix_tight_continuation_over_indented_unordered() {
780 let content = "- Item\n over-indented\n";
781 let fixed = fix(content);
782 assert_eq!(fixed, "- Item\n over-indented\n");
783 }
784
785 #[test]
786 fn fix_tight_continuation_multiple_lines() {
787 let content = "1. Item\n line one\n line two\n";
788 let fixed = fix(content);
789 assert_eq!(fixed, "1. Item\n line one\n line two\n");
790 }
791
792 #[test]
793 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
794 let content = "1. Item\n continuation\n";
797 assert!(check_mkdocs(content).is_empty());
798 }
799
800 #[test]
801 fn tight_continuation_mkdocs_5space_ordered_flagged() {
802 let content = "1. Item\n over-indented\n";
804 let warnings = check_mkdocs(content);
805 assert_eq!(warnings.len(), 1);
806 assert!(warnings[0].message.contains("expected 4"));
807 assert!(warnings[0].message.contains("found 5"));
808 }
809
810 #[test]
811 fn fix_tight_continuation_mkdocs_over_indented() {
812 let content = "1. Item\n over-indented\n";
813 let fixed = fix_mkdocs(content);
814 assert_eq!(fixed, "1. Item\n over-indented\n");
815 }
816
817 #[test]
818 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
819 let content = "* Level 0\n * Level 1\n * Level 2\n";
822 assert!(check(content).is_empty());
823 }
824
825 #[test]
826 fn tight_continuation_ordered_marker_not_flagged() {
827 let content = "- Parent\n 1. Child item\n";
829 assert!(check(content).is_empty());
830 }
831
832 #[test]
835 fn unordered_correct_indent_no_warning() {
836 let content = "- Item\n\n continuation\n";
837 assert!(check(content).is_empty());
838 }
839
840 #[test]
841 fn unordered_partial_indent_warns() {
842 let content = "- Item\n\n continuation\n";
845 let warnings = check(content);
846 assert_eq!(warnings.len(), 1);
847 assert_eq!(warnings[0].line, 3);
848 assert!(warnings[0].message.contains("2 spaces"));
849 assert!(warnings[0].message.contains("found 1"));
850 }
851
852 #[test]
853 fn unordered_zero_indent_is_new_paragraph() {
854 let content = "- Item\n\ncontinuation\n";
857 assert!(check(content).is_empty());
858 }
859
860 #[test]
863 fn ordered_3space_correct_commonmark() {
864 let content = "1. Item\n\n continuation\n";
866 assert!(check(content).is_empty());
867 }
868
869 #[test]
870 fn ordered_2space_under_indent_commonmark() {
871 let content = "1. Item\n\n continuation\n";
872 let warnings = check(content);
873 assert_eq!(warnings.len(), 1);
874 assert!(warnings[0].message.contains("3 spaces"));
875 assert!(warnings[0].message.contains("found 2"));
876 }
877
878 #[test]
881 fn multi_digit_marker_correct() {
882 let content = "10. Item\n\n continuation\n";
884 assert!(check(content).is_empty());
885 }
886
887 #[test]
888 fn multi_digit_marker_under_indent() {
889 let content = "10. Item\n\n continuation\n";
890 let warnings = check(content);
891 assert_eq!(warnings.len(), 1);
892 assert!(warnings[0].message.contains("4 spaces"));
893 }
894
895 #[test]
898 fn mkdocs_3space_ordered_warns() {
899 let content = "1. Item\n\n continuation\n";
901 let warnings = check_mkdocs(content);
902 assert_eq!(warnings.len(), 1);
903 assert!(warnings[0].message.contains("4 spaces"));
904 assert!(warnings[0].message.contains("MkDocs"));
905 }
906
907 #[test]
908 fn mkdocs_4space_ordered_no_warning() {
909 let content = "1. Item\n\n continuation\n";
910 assert!(check_mkdocs(content).is_empty());
911 }
912
913 #[test]
914 fn mkdocs_unordered_2space_ok() {
915 let content = "- Item\n\n continuation\n";
917 assert!(check_mkdocs(content).is_empty());
918 }
919
920 #[test]
921 fn mkdocs_unordered_2space_warns() {
922 let content = "- Item\n\n continuation\n";
924 let warnings = check_mkdocs(content);
925 assert_eq!(warnings.len(), 1);
926 assert!(warnings[0].message.contains("4 spaces"));
927 }
928
929 #[test]
932 fn fix_unordered_indent() {
933 let content = "- Item\n\n continuation\n";
935 let fixed = fix(content);
936 assert_eq!(fixed, "- Item\n\n continuation\n");
937 }
938
939 #[test]
940 fn fix_ordered_indent() {
941 let content = "1. Item\n\n continuation\n";
942 let fixed = fix(content);
943 assert_eq!(fixed, "1. Item\n\n continuation\n");
944 }
945
946 #[test]
947 fn fix_mkdocs_indent() {
948 let content = "1. Item\n\n continuation\n";
949 let fixed = fix_mkdocs(content);
950 assert_eq!(fixed, "1. Item\n\n continuation\n");
951 }
952
953 #[test]
956 fn nested_list_items_not_flagged() {
957 let content = "- Parent\n\n - Child\n";
958 assert!(check(content).is_empty());
959 }
960
961 #[test]
962 fn nested_list_zero_indent_is_new_paragraph() {
963 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
965 assert!(check(content).is_empty());
966 }
967
968 #[test]
969 fn nested_list_partial_indent_flagged() {
970 let content = "- Parent\n - Child\n\n continuation of parent\n";
972 let warnings = check(content);
973 assert_eq!(warnings.len(), 1);
974 assert!(warnings[0].message.contains("2 spaces"));
975 }
976
977 #[test]
980 fn code_block_correctly_indented_no_warning() {
981 let content = "- Item\n\n ```\n code\n ```\n";
983 assert!(check(content).is_empty());
984 }
985
986 #[test]
987 fn code_fence_under_indented_warns() {
988 let content = "- Item\n\n ```\n code\n ```\n";
992 let warnings = check(content);
993 assert_eq!(warnings.len(), 1);
994 assert_eq!(warnings[0].line, 3);
995 }
996
997 #[test]
998 fn code_fence_under_indented_ordered_mkdocs() {
999 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
1002 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
1004 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1006 assert!(warnings[0].message.contains("4 spaces"));
1007 assert!(warnings[0].message.contains("MkDocs"));
1008 }
1009
1010 #[test]
1011 fn code_fence_tilde_under_indented() {
1012 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1013 let warnings = check(content);
1014 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1016 }
1017
1018 #[test]
1021 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
1022 let content = "- Item\n\n\ncontinuation\n";
1024 assert!(check(content).is_empty());
1025 }
1026
1027 #[test]
1028 fn multiple_blank_lines_partial_indent_flags() {
1029 let content = "- Item\n\n\n continuation\n";
1030 let warnings = check(content);
1031 assert_eq!(warnings.len(), 1);
1032 }
1033
1034 #[test]
1037 fn empty_item_no_warning() {
1038 let content = "- \n- Second\n";
1039 assert!(check(content).is_empty());
1040 }
1041
1042 #[test]
1045 fn multiple_items_mixed_indent() {
1046 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
1047 let warnings = check(content);
1048 assert_eq!(warnings.len(), 1);
1049 assert_eq!(warnings[0].line, 7);
1050 }
1051
1052 #[test]
1055 fn task_list_correct_indent() {
1056 let content = "- [ ] Task\n\n continuation\n";
1058 assert!(check(content).is_empty());
1059 }
1060
1061 #[test]
1064 fn frontmatter_not_flagged() {
1065 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
1066 assert!(check(content).is_empty());
1067 }
1068
1069 #[test]
1072 fn fix_multiple_items() {
1073 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
1074 let fixed = fix(content);
1075 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
1076 }
1077
1078 #[test]
1079 fn fix_multiline_loose_continuation_all_lines() {
1080 let content = "1. Item\n\n line one\n line two\n line three\n";
1081 let fixed = fix(content);
1082 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
1083 }
1084
1085 #[test]
1088 fn sibling_item_boundary_respected() {
1089 let content = "- First\n- Second\n\n continuation\n";
1091 assert!(check(content).is_empty());
1092 }
1093
1094 #[test]
1097 fn blockquote_list_correct_indent_no_warning() {
1098 let content = "> - Item\n>\n> continuation\n";
1101 assert!(check(content).is_empty());
1102 }
1103
1104 #[test]
1105 fn blockquote_list_under_indent_no_false_positive() {
1106 let content = "> - Item\n>\n> continuation\n";
1111 assert!(check(content).is_empty());
1112 }
1113
1114 #[test]
1117 fn deeply_nested_correct_indent() {
1118 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1119 assert!(check(content).is_empty());
1120 }
1121
1122 #[test]
1123 fn deeply_nested_under_indent() {
1124 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1127 let warnings = check(content);
1128 assert_eq!(warnings.len(), 1);
1129 assert!(warnings[0].message.contains("6 spaces"));
1130 assert!(warnings[0].message.contains("found 5"));
1131 }
1132
1133 #[test]
1136 fn loose_tab_continuation_over_indented() {
1137 let content = "- Item\n\n\tcontinuation\n";
1142 let warnings = check(content);
1143 assert_eq!(warnings.len(), 1);
1144 assert_eq!(warnings[0].line, 3);
1145 assert_eq!(fix(content), "- Item\n\n continuation\n");
1146 }
1147
1148 #[test]
1151 fn multiple_continuations_correct() {
1152 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
1153 assert!(check(content).is_empty());
1154 }
1155
1156 #[test]
1157 fn multiple_continuations_second_under_indent() {
1158 let content = "- Item\n\n para 1\n\n continuation 2\n";
1160 let warnings = check(content);
1161 assert_eq!(warnings.len(), 1);
1162 assert_eq!(warnings[0].line, 5);
1163 }
1164
1165 #[test]
1168 fn ordered_paren_marker_correct() {
1169 let content = "1) Item\n\n continuation\n";
1171 assert!(check(content).is_empty());
1172 }
1173
1174 #[test]
1175 fn ordered_paren_marker_under_indent() {
1176 let content = "1) Item\n\n continuation\n";
1177 let warnings = check(content);
1178 assert_eq!(warnings.len(), 1);
1179 assert!(warnings[0].message.contains("3 spaces"));
1180 }
1181
1182 #[test]
1185 fn star_marker_correct() {
1186 let content = "* Item\n\n continuation\n";
1187 assert!(check(content).is_empty());
1188 }
1189
1190 #[test]
1191 fn star_marker_under_indent() {
1192 let content = "* Item\n\n continuation\n";
1193 let warnings = check(content);
1194 assert_eq!(warnings.len(), 1);
1195 }
1196
1197 #[test]
1198 fn plus_marker_correct() {
1199 let content = "+ Item\n\n continuation\n";
1200 assert!(check(content).is_empty());
1201 }
1202
1203 #[test]
1206 fn heading_after_list_no_warning() {
1207 let content = "- Item\n\n# Heading\n";
1208 assert!(check(content).is_empty());
1209 }
1210
1211 #[test]
1214 fn hr_after_list_no_warning() {
1215 let content = "- Item\n\n---\n";
1216 assert!(check(content).is_empty());
1217 }
1218
1219 #[test]
1222 fn reference_link_def_not_flagged() {
1223 let content = "- Item\n\n [link]: https://example.com\n";
1224 assert!(check(content).is_empty());
1225 }
1226
1227 #[test]
1230 fn footnote_def_not_flagged() {
1231 let content = "- Item\n\n [^1]: footnote text\n";
1232 assert!(check(content).is_empty());
1233 }
1234
1235 #[test]
1238 fn fix_deeply_nested() {
1239 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
1240 let fixed = fix(content);
1241 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
1242 }
1243
1244 #[test]
1245 fn fix_mkdocs_unordered() {
1246 let content = "- Item\n\n continuation\n";
1248 let fixed = fix_mkdocs(content);
1249 assert_eq!(fixed, "- Item\n\n continuation\n");
1250 }
1251
1252 #[test]
1253 fn fix_code_fence_indent() {
1254 let content = "- Item\n\n ```\n code\n ```\n";
1257 let fixed = fix(content);
1258 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
1259 }
1260
1261 #[test]
1262 fn fix_mkdocs_code_fence_indent() {
1263 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
1265 let fixed = fix_mkdocs(content);
1266 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
1267 }
1268
1269 #[test]
1272 fn empty_document_no_warning() {
1273 assert!(check("").is_empty());
1274 }
1275
1276 #[test]
1277 fn whitespace_only_no_warning() {
1278 assert!(check(" \n\n \n").is_empty());
1279 }
1280
1281 #[test]
1284 fn no_list_no_warning() {
1285 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
1286 assert!(check(content).is_empty());
1287 }
1288
1289 #[test]
1292 fn multiline_continuation_all_lines_flagged() {
1293 let content = "1. This is a list item.\n\n This is continuation text and\n it has multiple lines.\n This is yet another line.\n";
1294 let warnings = check(content);
1295 assert_eq!(warnings.len(), 3);
1296 assert_eq!(warnings[0].line, 3);
1297 assert_eq!(warnings[1].line, 4);
1298 assert_eq!(warnings[2].line, 5);
1299 }
1300
1301 #[test]
1302 fn multiline_continuation_with_frontmatter_fix() {
1303 let content = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n1. This is a list item.\n\n This is list continuation text and\n it has multiple lines that aren't indented properly.\n This is yet another line that isn't indented properly.\n1. This is a list item.\n\n This is list continuation text and\n it has multiple lines that aren't indented properly.\n This is yet another line that isn't indented properly.\n";
1304 let fixed = fix(content);
1305 assert_eq!(
1306 fixed,
1307 "---\ntitle: Heading\n---\n\nSome introductory text:\n\n1. This is a list item.\n\n This is list continuation text and\n it has multiple lines that aren't indented properly.\n This is yet another line that isn't indented properly.\n1. This is a list item.\n\n This is list continuation text and\n it has multiple lines that aren't indented properly.\n This is yet another line that isn't indented properly.\n"
1308 );
1309 }
1310
1311 #[test]
1312 fn multiline_continuation_correct_indent_no_warning() {
1313 let content = "1. Item\n\n line one\n line two\n line three\n";
1314 assert!(check(content).is_empty());
1315 }
1316
1317 #[test]
1318 fn multiline_continuation_mixed_indent() {
1319 let content = "1. Item\n\n correct\n wrong\n correct\n";
1320 let warnings = check(content);
1321 assert_eq!(warnings.len(), 1);
1322 assert_eq!(warnings[0].line, 4);
1323 }
1324
1325 #[test]
1326 fn multiline_continuation_unordered() {
1327 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
1328 let warnings = check(content);
1329 assert_eq!(warnings.len(), 3);
1330 let fixed = fix(content);
1331 assert_eq!(
1332 fixed,
1333 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
1334 );
1335 }
1336
1337 #[test]
1338 fn multiline_continuation_two_items_fix() {
1339 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
1340 let fixed = fix(content);
1341 assert_eq!(
1342 fixed,
1343 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1344 );
1345 }
1346
1347 #[test]
1348 fn fence_fix_does_not_break_pairing_for_md031() {
1349 let content = "#### title\n\nabc\n\n\
1356 1. ab\n\n\
1357 \x20\x20`aabbccdd`\n\n\
1358 2. cd\n\n\
1359 \x20\x20`bbcc dd ee`\n\n\
1360 \x20\x20```\n\
1361 \x20\x20abcd\n\
1362 \x20\x20ef gh\n\
1363 \x20\x20```\n\n\
1364 \x20\x20uu\n\n\
1365 \x20\x20```\n\
1366 \x20\x20cdef\n\
1367 \x20\x20gh ij\n\
1368 \x20\x20```\n";
1369 let expected = "#### title\n\nabc\n\n\
1370 1. ab\n\n\
1371 \x20\x20\x20`aabbccdd`\n\n\
1372 2. cd\n\n\
1373 \x20\x20\x20`bbcc dd ee`\n\n\
1374 \x20\x20\x20```\n\
1375 \x20\x20\x20abcd\n\
1376 \x20\x20\x20ef gh\n\
1377 \x20\x20\x20```\n\n\
1378 \x20\x20\x20uu\n\n\
1379 \x20\x20\x20```\n\
1380 \x20\x20\x20cdef\n\
1381 \x20\x20\x20gh ij\n\
1382 \x20\x20\x20```\n";
1383 assert_eq!(fix(content), expected);
1384 }
1385
1386 #[test]
1387 fn multiline_continuation_separated_by_blank() {
1388 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1389 let warnings = check(content);
1390 assert_eq!(warnings.len(), 4);
1391 let fixed = fix(content);
1392 assert_eq!(
1393 fixed,
1394 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1395 );
1396 }
1397
1398 #[test]
1399 fn tab_indented_fence_is_normalized_to_spaces() {
1400 let content = "100. ab\n\n\t```\n\tabcd\n\t```\n";
1408 let expected = "100. ab\n\n ```\n abcd\n ```\n";
1409 assert_eq!(fix(content), expected);
1410 }
1411
1412 #[test]
1421 fn loose_continuation_over_indented_flagged() {
1422 let content = "* Item\n\n over-indented\n";
1425 let warnings = check(content);
1426 assert_eq!(warnings.len(), 1);
1427 assert_eq!(warnings[0].line, 3);
1428 assert!(warnings[0].message.contains("over-indented"));
1429 assert!(warnings[0].message.contains("expected 2"));
1430 assert!(warnings[0].message.contains("found 3"));
1431 }
1432
1433 #[test]
1434 fn loose_continuation_over_indented_multiline_mixed() {
1435 let content = "* Item\n\n over one\n correct\n over two\n";
1437 let warnings = check(content);
1438 assert_eq!(warnings.len(), 2);
1439 assert_eq!(warnings[0].line, 3);
1440 assert_eq!(warnings[1].line, 5);
1441 }
1442
1443 #[test]
1444 fn fix_loose_continuation_over_indented() {
1445 let content = "* Item\n\n over one\n correct\n over two\n";
1446 let fixed = fix(content);
1447 assert_eq!(fixed, "* Item\n\n over one\n correct\n over two\n");
1448 }
1449
1450 #[test]
1451 fn fix_tight_and_loose_items_normalized_identically() {
1452 let content = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1455 * This is a list item.\n This is list continuation text and\n it has multiple lines that aren't indented properly.\n This is yet another line that isn't indented properly.\n\n\
1456 * This is a list item.\n\n This is list continuation text and\n it has multiple lines that aren't indented properly.\n This is yet another line that isn't indented properly.\n";
1457 let expected = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1458 * This is a list item.\n This is list continuation text and\n it has multiple lines that aren't indented properly.\n This is yet another line that isn't indented properly.\n\n\
1459 * This is a list item.\n\n This is list continuation text and\n it has multiple lines that aren't indented properly.\n This is yet another line that isn't indented properly.\n";
1460 assert_eq!(fix(content), expected);
1461 }
1462
1463 #[test]
1464 fn multi_paragraph_item_loose_paragraph_over_indented() {
1465 let content = "* Item.\n tight over\n\n loose over\n";
1468 let warnings = check(content);
1469 assert_eq!(warnings.len(), 2);
1470 assert_eq!(warnings[0].line, 2);
1471 assert_eq!(warnings[1].line, 4);
1472 }
1473
1474 #[test]
1475 fn loose_indented_code_block_not_flagged() {
1476 let content = "- Item\n\n code line\n";
1480 assert!(check(content).is_empty());
1481 }
1482
1483 #[test]
1484 fn mkdocs_loose_over_indented_flagged() {
1485 let content = "1. Item\n\n over\n";
1488 let warnings = check_mkdocs(content);
1489 assert_eq!(warnings.len(), 1);
1490 assert_eq!(warnings[0].line, 3);
1491 assert!(warnings[0].message.contains("over-indented"));
1492 assert!(warnings[0].message.contains("expected 4"));
1493 assert!(warnings[0].message.contains("found 5"));
1494 }
1495
1496 #[test]
1497 fn task_list_loose_over_indented_flagged() {
1498 let content = "- [ ] Task\n\n over\n";
1501 let warnings = check(content);
1502 assert_eq!(warnings.len(), 1);
1503 assert_eq!(warnings[0].line, 3);
1504 }
1505
1506 #[test]
1507 fn loose_over_indent_boundary_below_code_block_threshold_flagged() {
1508 let content = "- Item\n\n over\n";
1513 let warnings = check(content);
1514 assert_eq!(warnings.len(), 1);
1515 assert_eq!(warnings[0].line, 3);
1516 assert!(warnings[0].message.contains("expected 2"));
1517 assert!(warnings[0].message.contains("found 5"));
1518 }
1519
1520 #[test]
1521 fn loose_over_indent_does_not_steal_nested_under_indent() {
1522 let content = "- Outer\n - Inner\n\n continuation\n";
1529 let warnings = check(content);
1530 assert_eq!(warnings.len(), 1);
1531 assert_eq!(warnings[0].line, 4);
1532 assert!(warnings[0].message.contains("4 spaces"));
1533 assert!(warnings[0].message.contains("found 3"));
1534 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1535 }
1536
1537 #[test]
1538 fn loose_over_indent_attributes_to_deepest_enclosing_item() {
1539 let content = "- Outer\n - Inner\n\n continuation\n";
1543 let warnings = check(content);
1544 assert_eq!(warnings.len(), 1);
1545 assert_eq!(warnings[0].line, 4);
1546 assert!(warnings[0].message.contains("expected 4"));
1547 assert!(warnings[0].message.contains("found 5"));
1548 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1549 }
1550
1551 #[test]
1560 fn loose_over_indented_fence_not_flagged() {
1561 let content = "- Item\n\n ```\n code\n ```\n";
1562 assert!(check(content).is_empty());
1563 assert_eq!(fix(content), content);
1564 }
1565
1566 #[test]
1567 fn tight_over_indented_fence_not_flagged() {
1568 let content = "- Item\n ```\n code\n ```\n";
1569 assert!(check(content).is_empty());
1570 assert_eq!(fix(content), content);
1571 }
1572
1573 #[test]
1574 fn over_indented_tilde_fence_not_flagged() {
1575 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1576 assert!(check(content).is_empty());
1577 assert_eq!(fix(content), content);
1578 }
1579
1580 #[test]
1581 fn fence_like_code_content_inside_fenced_block_not_flagged() {
1582 let content = "- Item\n\n ~~~\n ```\n ~~~\n";
1585 assert!(check(content).is_empty());
1586 assert_eq!(fix(content), content);
1587 }
1588
1589 #[test]
1590 fn unterminated_over_indented_fence_not_flagged() {
1591 let content = "- Item\n\n ```\n code1\n code2deeper\n";
1594 assert!(check(content).is_empty());
1595 assert_eq!(fix(content), content);
1596 }
1597
1598 #[test]
1606 fn task_list_tight_continuation_post_checkbox_reproducer_579() {
1607 let content = "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n";
1610 assert!(check(content).is_empty());
1611 }
1612
1613 #[test]
1614 fn task_list_tight_continuation_dash_unchecked() {
1615 let content = "- [ ] Task\n continuation\n";
1616 assert!(check(content).is_empty());
1617 }
1618
1619 #[test]
1620 fn task_list_tight_continuation_dash_checked_lower() {
1621 let content = "- [x] Task\n continuation\n";
1622 assert!(check(content).is_empty());
1623 }
1624
1625 #[test]
1626 fn task_list_tight_continuation_dash_checked_upper() {
1627 let content = "- [X] Task\n continuation\n";
1628 assert!(check(content).is_empty());
1629 }
1630
1631 #[test]
1632 fn task_list_tight_continuation_star_marker() {
1633 let content = "* [ ] Task\n continuation\n";
1634 assert!(check(content).is_empty());
1635 }
1636
1637 #[test]
1638 fn task_list_tight_continuation_plus_marker() {
1639 let content = "+ [ ] Task\n continuation\n";
1640 assert!(check(content).is_empty());
1641 }
1642
1643 #[test]
1644 fn task_list_tight_continuation_content_column_still_valid() {
1645 let content = "- [ ] Task\n continuation\n";
1648 assert!(check(content).is_empty());
1649 }
1650
1651 #[test]
1652 fn task_list_tight_continuation_between_columns_still_flagged() {
1653 let content = "- [ ] Task\n continuation\n";
1656 let warnings = check(content);
1657 assert_eq!(warnings.len(), 1);
1658 assert!(warnings[0].message.contains("expected 2 or 6"));
1660 assert!(warnings[0].message.contains("found 4"));
1661 }
1662
1663 #[test]
1664 fn task_list_tight_continuation_overshoot_still_flagged() {
1665 let content = "- [ ] Task\n continuation\n";
1667 let warnings = check(content);
1668 assert_eq!(warnings.len(), 1);
1669 assert!(warnings[0].message.contains("expected 2 or 6"));
1670 assert!(warnings[0].message.contains("found 7"));
1671 }
1672
1673 #[test]
1676 fn fix_task_list_overshoot_snaps_to_task_col() {
1677 let content = "- [ ] Task\n continuation\n";
1681 let fixed = fix(content);
1682 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1683 }
1684
1685 #[test]
1686 fn fix_task_list_col_5_snaps_to_task_col() {
1687 let content = "- [ ] Task\n continuation\n";
1689 let fixed = fix(content);
1690 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1691 }
1692
1693 #[test]
1694 fn fix_task_list_col_3_snaps_to_content_col() {
1695 let content = "- [ ] Task\n continuation\n";
1697 let fixed = fix(content);
1698 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1699 }
1700
1701 #[test]
1702 fn fix_task_list_col_4_ties_to_content_col() {
1703 let content = "- [ ] Task\n continuation\n";
1708 let fixed = fix(content);
1709 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1710 }
1711
1712 #[test]
1713 fn fix_task_list_ordered_overshoot_snaps_to_task_col() {
1714 let content = "1. [ ] Task\n continuation\n";
1717 let fixed = fix(content);
1718 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1719 }
1720
1721 #[test]
1722 fn fix_task_list_ordered_under_overshoot_snaps_to_content_col() {
1723 let content = "1. [ ] Task\n continuation\n";
1726 let fixed = fix(content);
1727 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1728 }
1729
1730 #[test]
1731 fn task_list_tight_continuation_ordered_single_digit() {
1732 let content = "1. [ ] Task\n continuation\n";
1734 assert!(check(content).is_empty());
1735 }
1736
1737 #[test]
1738 fn task_list_tight_continuation_ordered_multi_digit() {
1739 let content = "10. [ ] Task\n continuation\n";
1741 assert!(check(content).is_empty());
1742 }
1743
1744 #[test]
1745 fn task_list_tight_continuation_nested_dash() {
1746 let content = "- Parent\n - [ ] Nested task\n continuation\n";
1748 assert!(check(content).is_empty());
1749 }
1750
1751 #[test]
1752 fn task_list_loose_continuation_post_checkbox_column_not_flagged() {
1753 let content = "- [ ] Task\n\n continuation\n";
1758 assert!(check(content).is_empty());
1759 }
1760
1761 #[test]
1762 fn task_list_empty_body_is_not_a_task() {
1763 let content = "- [ ]\n continuation\n";
1769 let warnings = check(content);
1770 assert_eq!(warnings.len(), 1);
1771 assert!(warnings[0].message.contains("found 4"));
1772 }
1773
1774 #[test]
1775 fn task_list_malformed_checkbox_is_not_a_task() {
1776 let content = "- [~] Not a task\n continuation\n";
1778 let warnings = check(content);
1779 assert_eq!(warnings.len(), 1);
1780 }
1781
1782 #[test]
1789 fn task_list_mkdocs_unordered_required_min_valid() {
1790 let content = "- [ ] Task\n continuation\n";
1792 assert!(check_mkdocs(content).is_empty());
1793 }
1794
1795 #[test]
1796 fn task_list_mkdocs_unordered_post_checkbox_valid() {
1797 let content = "- [ ] Task\n continuation\n";
1798 assert!(check_mkdocs(content).is_empty());
1799 }
1800
1801 #[test]
1802 fn task_list_mkdocs_unordered_between_flagged() {
1803 let content = "- [ ] Task\n continuation\n";
1805 let warnings = check_mkdocs(content);
1806 assert_eq!(warnings.len(), 1);
1807 }
1808
1809 #[test]
1810 fn task_list_mkdocs_ordered_both_columns_valid() {
1811 let at_4 = "1. [ ] Task\n continuation\n";
1813 assert!(check_mkdocs(at_4).is_empty());
1814 let at_7 = "1. [ ] Task\n continuation\n";
1815 assert!(check_mkdocs(at_7).is_empty());
1816 }
1817
1818 #[test]
1819 fn task_list_mkdocs_ordered_between_flagged() {
1820 let at_5 = "1. [ ] Task\n continuation\n";
1822 assert_eq!(check_mkdocs(at_5).len(), 1);
1823 let at_6 = "1. [ ] Task\n continuation\n";
1824 assert_eq!(check_mkdocs(at_6).len(), 1);
1825 }
1826
1827 #[test]
1837 fn fix_task_list_tie_sibling_at_task_col_snaps_to_task_col() {
1838 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
1842 let fixed = fix(content);
1843 assert_eq!(
1844 fixed,
1845 "- [ ] Task\n aligned continuation\n tied continuation\n"
1846 );
1847 }
1848
1849 #[test]
1850 fn fix_task_list_tie_sibling_at_content_col_snaps_to_content_col() {
1851 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
1854 let fixed = fix(content);
1855 assert_eq!(fixed, "- [ ] Task\n aligned continuation\n tied continuation\n");
1856 }
1857
1858 #[test]
1859 fn fix_task_list_tie_both_siblings_snaps_to_content_col() {
1860 let content = "- [ ] Task\n at content col\n at task col\n tied continuation\n";
1864 let fixed = fix(content);
1865 assert_eq!(
1866 fixed,
1867 "- [ ] Task\n at content col\n at task col\n tied continuation\n"
1868 );
1869 }
1870
1871 #[test]
1872 fn fix_task_list_tie_sees_task_col_through_tight_lazy_continuation() {
1873 let content = concat!("- [ ] Task\n", "lazy\n", " aligned at task col\n", " tied\n",);
1887 let fixed = fix(content);
1888 assert!(
1889 fixed.contains("\n tied\n"),
1890 "tied line should snap to col 6 (task col) because a task-col \
1891 sibling is visible past the tight lazy-continuation line; got:\n{fixed}"
1892 );
1893 }
1894
1895 #[test]
1902 fn task_list_tab_indented_continuation_flagged() {
1903 let content = "- [ ] Task\n\t\twrap\n";
1906 let warnings = check(content);
1907 assert_eq!(warnings.len(), 1);
1908 assert!(warnings[0].message.contains("expected 2 or 6"));
1909 assert!(warnings[0].message.contains("found 8"));
1910 }
1911
1912 #[test]
1913 fn fix_task_list_tab_indented_snaps_to_task_col() {
1914 let content = "- [ ] Task\n\t\twrap\n";
1916 let fixed = fix(content);
1917 assert_eq!(fixed, "- [ ] Task\n wrap\n");
1918 }
1919
1920 #[test]
1921 fn fix_task_list_single_tab_equidistant_snaps_to_content_col() {
1922 let content = "- [ ] Task\n\twrap\n";
1925 let fixed = fix(content);
1926 assert_eq!(fixed, "- [ ] Task\n wrap\n");
1927 }
1928
1929 #[test]
1939 fn task_list_blockquote_post_checkbox_not_flagged() {
1940 let content = "> - [ ] Task\n> continuation\n";
1942 assert!(check(content).is_empty());
1943 }
1944
1945 #[test]
1946 fn task_list_blockquote_between_cols_documented_limitation() {
1947 let content = "> - [ ] Task\n> continuation\n";
1951 assert!(check(content).is_empty());
1952 }
1953
1954 #[test]
1955 fn task_list_blockquote_overshoot_documented_limitation() {
1956 let content = "> - [ ] Task\n> continuation\n";
1958 assert!(check(content).is_empty());
1959 }
1960
1961 #[test]
1968 fn fix_task_list_mkdocs_unordered_overshoot_snaps_to_task_col() {
1969 let content = "- [ ] Task\n continuation\n";
1972 let fixed = fix_mkdocs(content);
1973 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1974 }
1975
1976 #[test]
1977 fn fix_task_list_mkdocs_unordered_tie_snaps_to_required() {
1978 let content = "- [ ] Task\n continuation\n";
1981 let fixed = fix_mkdocs(content);
1982 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1983 }
1984
1985 #[test]
1986 fn fix_task_list_mkdocs_ordered_overshoot_snaps_to_task_col() {
1987 let content = "1. [ ] Task\n continuation\n";
1990 let fixed = fix_mkdocs(content);
1991 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1992 }
1993
1994 #[test]
1995 fn fix_task_list_mkdocs_ordered_near_required_snaps_to_required() {
1996 let content = "1. [ ] Task\n continuation\n";
2002 let fixed = fix_mkdocs(content);
2003 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2004 }
2005
2006 #[test]
2007 fn fix_task_list_mkdocs_ordered_between_cols_snaps_to_task_col() {
2008 let content = "1. [ ] Task\n continuation\n";
2011 let fixed = fix_mkdocs(content);
2012 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2013 }
2014
2015 fn assert_idempotent(content: &str) {
2025 let once = fix(content);
2026 let twice = fix(&once);
2027 assert_eq!(once, twice, "MD077 fix was not idempotent on input: {content:?}");
2028 }
2029
2030 fn assert_idempotent_mkdocs(content: &str) {
2031 let once = fix_mkdocs(content);
2032 let twice = fix_mkdocs(&once);
2033 assert_eq!(
2034 once, twice,
2035 "MD077 (MkDocs) fix was not idempotent on input: {content:?}"
2036 );
2037 }
2038
2039 #[test]
2040 fn idempotent_task_list_between_cols() {
2041 assert_idempotent("- [ ] Task\n continuation\n");
2042 }
2043
2044 #[test]
2045 fn idempotent_task_list_overshoot() {
2046 assert_idempotent("- [ ] Task\n continuation\n");
2047 }
2048
2049 #[test]
2050 fn idempotent_task_list_under_post_checkbox() {
2051 assert_idempotent("- [ ] Task\n continuation\n");
2052 }
2053
2054 #[test]
2055 fn idempotent_task_list_near_post_checkbox() {
2056 assert_idempotent("- [ ] Task\n continuation\n");
2057 }
2058
2059 #[test]
2060 fn idempotent_task_list_tab_overshoot() {
2061 assert_idempotent("- [ ] Task\n\t\twrap\n");
2062 }
2063
2064 #[test]
2065 fn idempotent_task_list_single_tab() {
2066 assert_idempotent("- [ ] Task\n\twrap\n");
2067 }
2068
2069 #[test]
2070 fn idempotent_task_list_ordered_overshoot() {
2071 assert_idempotent("1. [ ] Task\n continuation\n");
2072 }
2073
2074 #[test]
2075 fn idempotent_task_list_ordered_under() {
2076 assert_idempotent("1. [ ] Task\n continuation\n");
2077 }
2078
2079 #[test]
2080 fn idempotent_task_list_tie_with_sibling_at_task_col() {
2081 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2082 }
2083
2084 #[test]
2085 fn idempotent_task_list_tie_with_sibling_at_content_col() {
2086 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2087 }
2088
2089 #[test]
2090 fn idempotent_task_list_mkdocs_unordered_overshoot() {
2091 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2092 }
2093
2094 #[test]
2095 fn idempotent_task_list_mkdocs_unordered_tie() {
2096 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2097 }
2098
2099 #[test]
2100 fn idempotent_task_list_mkdocs_ordered_overshoot() {
2101 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2102 }
2103
2104 #[test]
2105 fn idempotent_task_list_mkdocs_ordered_between() {
2106 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2107 }
2108
2109 #[test]
2110 fn idempotent_task_list_reproducer_579() {
2111 assert_idempotent(
2115 "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n",
2116 );
2117 }
2118
2119 #[test]
2120 fn idempotent_non_task_list_still_holds() {
2121 assert_idempotent("1. Item\n over-indented\n");
2124 assert_idempotent("- Item\n\n continuation\n");
2125 }
2126
2127 #[test]
2134 fn idempotent_non_task_loose_under_indent_ordered() {
2135 assert_idempotent("1. Item\n\n continuation\n");
2137 }
2138
2139 #[test]
2140 fn idempotent_non_task_loose_under_indent_multi_digit() {
2141 assert_idempotent("10. Item\n\n continuation\n");
2143 }
2144
2145 #[test]
2146 fn idempotent_non_task_tight_over_indent_ordered() {
2147 assert_idempotent("1. Item\n over-indented\n");
2149 }
2150
2151 #[test]
2159 fn idempotent_non_task_fence_ordered_loose() {
2160 assert_idempotent("1. Item\n\n ```rust\n let x = 1;\n ```\n");
2162 }
2163
2164 #[test]
2165 fn idempotent_non_task_fence_tilde_under_indent() {
2166 assert_idempotent("1. Item\n\n ~~~\nplain text\n ~~~\n");
2172 }
2173
2174 #[test]
2175 fn idempotent_non_task_fence_interior_above_required() {
2176 assert_idempotent("1. Item\n\n ```\n deeply indented code\n ```\n");
2180 }
2181
2182 #[test]
2183 fn fence_fix_promotes_interior_below_scope_in_single_pass() {
2184 let content = "1. Item\n\n ```\ncode\n ```\n";
2188 let fixed = fix(content);
2189 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2190 }
2191
2192 #[test]
2193 fn fence_fix_preserves_interior_above_required() {
2194 let content = "1. Item\n\n ```\n code\n ```\n";
2197 let fixed = fix(content);
2198 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2199 }
2200
2201 #[test]
2208 fn idempotent_non_task_mkdocs_ordered_at_3_spaces() {
2209 assert_idempotent_mkdocs("1. Item\n\n continuation\n");
2211 }
2212
2213 #[test]
2214 fn idempotent_non_task_mkdocs_unordered_at_2_spaces() {
2215 assert_idempotent_mkdocs("- Item\n\n continuation\n");
2217 }
2218
2219 #[test]
2220 fn idempotent_non_task_mkdocs_fence_compound() {
2221 assert_idempotent_mkdocs("1. Item\n\n ```toml\n k = 1\n ```\n");
2223 }
2224}