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 {
348 if info.in_code_block && !Self::is_code_fence(trimmed) {
349 return true;
350 }
351 info.in_front_matter
352 || info.in_footnote_definition
353 || info.in_html_block
354 || info.in_html_comment
355 || info.in_mdx_comment
356 || info.in_mkdocstrings
357 || info.in_esm_block
358 || info.in_math_block
359 || info.in_admonition
360 || info.in_content_tab
361 || info.in_pymdown_block
362 || info.in_definition_list
363 || info.in_mkdocs_html_markdown
364 || info.in_kramdown_extension_block
365 }
366
367 fn build_over_indent_warning(
376 ctx: &LintContext,
377 line: &ContinuationLine<'_>,
378 fix_target: usize,
379 message: String,
380 ) -> LintWarning {
381 let line_content = line.info.content(ctx.content);
382 let fix_start = line.info.byte_offset;
383 let fix_end = fix_start + line.info.indent;
384 LintWarning {
385 rule_name: Some("MD077".to_string()),
386 line: line.line_num,
387 column: 1,
388 end_line: line.line_num,
389 end_column: line_content.chars().count() + 1,
390 message,
391 severity: Severity::Warning,
392 fix: Some(Fix::new(fix_start..fix_end, " ".repeat(fix_target))),
393 }
394 }
395
396 fn build_under_indent_warning(
408 ctx: &LintContext,
409 line: &ContinuationLine<'_>,
410 required: usize,
411 message: String,
412 ) -> UnderIndentOutcome {
413 let line_content = line.info.content(ctx.content);
414 let is_fence_opener = line.info.in_code_block
415 && Self::is_code_fence(line.trimmed)
416 && ctx.line_info(line.line_num - 1).is_none_or(|p| !p.in_code_block);
417
418 let (fix, warn_end_line, warn_end_column, compound_closer) = if is_fence_opener {
419 let closer_line = Self::find_fence_closer(ctx, line.line_num);
420 let fix = Self::build_compound_fence_fix(ctx, line.line_num, closer_line, line.actual, required);
421 let end_column = ctx
422 .line_info(closer_line)
423 .map_or(line_content.chars().count() + 1, |ci| {
424 ci.content(ctx.content).chars().count() + 1
425 });
426 let extra_flag = (closer_line != line.line_num).then_some(closer_line);
427 (fix, closer_line, end_column, extra_flag)
428 } else {
429 let fix_start = line.info.byte_offset;
430 let fix_end = fix_start + line.info.indent;
431 let fix = Some(Fix::new(fix_start..fix_end, " ".repeat(required)));
432 (fix, line.line_num, line_content.chars().count() + 1, None)
433 };
434
435 UnderIndentOutcome {
436 warning: LintWarning {
437 rule_name: Some("MD077".to_string()),
438 line: line.line_num,
439 column: 1,
440 end_line: warn_end_line,
441 end_column: warn_end_column,
442 message,
443 severity: Severity::Warning,
444 fix,
445 },
446 also_flag_line: compound_closer,
447 }
448 }
449}
450
451struct ContinuationLine<'a> {
455 line_num: usize,
456 info: &'a LineInfo,
457 trimmed: &'a str,
458 actual: usize,
459 saw_blank: bool,
460}
461
462struct UnderIndentOutcome {
467 warning: LintWarning,
468 also_flag_line: Option<usize>,
469}
470
471impl Rule for MD077ListContinuationIndent {
472 fn name(&self) -> &'static str {
473 "MD077"
474 }
475
476 fn description(&self) -> &'static str {
477 "List continuation content indentation"
478 }
479
480 fn check(&self, ctx: &LintContext) -> LintResult {
481 if ctx.content.is_empty() {
482 return Ok(Vec::new());
483 }
484
485 let strict_indent = ctx.flavor.requires_strict_list_indent();
486 let total_lines = ctx.lines.len();
487 let mut warnings = Vec::new();
488 let mut flagged_lines = std::collections::HashSet::new();
489
490 let mut items: Vec<(usize, usize, usize, Option<usize>)> = Vec::new();
499 for block in &ctx.list_blocks {
500 for &item_line in &block.item_lines {
501 if let Some(info) = ctx.line_info(item_line)
502 && let Some(ref li) = info.list_item
503 {
504 let line = info.content(ctx.content);
505 let task_col = Self::is_task_list_item(line, li.content_column)
506 .then_some(li.content_column + Self::TASK_CHECKBOX_PREFIX_LEN);
507 items.push((item_line, li.marker_column, li.content_column, task_col));
508 }
509 }
510 }
511 items.sort_unstable();
512 items.dedup_by_key(|&mut (ln, _, _, _)| ln);
513
514 let scoped: Vec<(usize, usize, usize, Option<usize>, usize, usize)> = items
518 .iter()
519 .enumerate()
520 .map(|(item_idx, &(item_line, marker_col, content_col, task_col))| {
521 let required = if strict_indent { content_col.max(4) } else { content_col };
522 let range_end = items
523 .iter()
524 .skip(item_idx + 1)
525 .find(|&&(_, mc, _, _)| mc <= marker_col)
526 .map_or(total_lines, |&(ln, _, _, _)| ln - 1);
527 (item_line, marker_col, content_col, task_col, required, range_end)
528 })
529 .collect();
530
531 for &(item_line, marker_col, _content_col, _task_col, required, range_end) in &scoped {
539 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
540 let actual = line.actual;
541 if line.saw_blank && actual < required && flagged_lines.insert(line.line_num) {
542 let message = if strict_indent {
543 format!(
544 "Content inside list item needs {required} spaces of indentation \
545 for MkDocs compatibility (found {actual})",
546 )
547 } else {
548 format!(
549 "Content after blank line in list item needs {required} spaces of \
550 indentation to remain part of the list (found {actual})",
551 )
552 };
553 let outcome = Self::build_under_indent_warning(ctx, line, required, message);
554 if let Some(closer_line) = outcome.also_flag_line {
555 flagged_lines.insert(closer_line);
556 }
557 warnings.push(outcome.warning);
558 }
559 ControlFlow::Continue(())
560 });
561 }
562
563 for &(item_line, marker_col, content_col, task_col, required, range_end) in &scoped {
572 let (uses_content_col, uses_task_col) = match task_col {
576 Some(t) => Self::sibling_column_usage(ctx, item_line, range_end, marker_col, content_col, t),
577 None => (false, false),
578 };
579
580 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
581 let actual = line.actual;
582 if actual > required
583 && !line.info.in_code_block
584 && Some(actual) != task_col
585 && !Self::starts_with_list_marker(line.trimmed)
586 && flagged_lines.insert(line.line_num)
587 {
588 let fix_target =
589 Self::compute_fix_target(actual, required, task_col, uses_content_col, uses_task_col);
590 let message = match task_col {
591 Some(t) => format!(
592 "Continuation line over-indented \
593 (expected {required} or {t}, found {actual})"
594 ),
595 None => {
596 format!("Continuation line over-indented (expected {required}, found {actual})")
597 }
598 };
599 warnings.push(Self::build_over_indent_warning(ctx, line, fix_target, message));
600 }
601 ControlFlow::Continue(())
602 });
603 }
604
605 warnings.sort_by_key(|w| (w.line, w.column));
608
609 Ok(warnings)
610 }
611
612 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
613 let warnings = self.check(ctx)?;
614 let warnings =
615 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
616 if warnings.is_empty() {
617 return Ok(ctx.content.to_string());
618 }
619
620 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
622 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
623
624 let mut content = ctx.content.to_string();
625 for fix in fixes {
626 if fix.range.start <= content.len() && fix.range.end <= content.len() {
627 content.replace_range(fix.range, &fix.replacement);
628 }
629 }
630
631 Ok(content)
632 }
633
634 fn category(&self) -> RuleCategory {
635 RuleCategory::List
636 }
637
638 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
639 ctx.content.is_empty() || ctx.list_blocks.is_empty()
640 }
641
642 fn as_any(&self) -> &dyn std::any::Any {
643 self
644 }
645
646 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
647 where
648 Self: Sized,
649 {
650 Box::new(Self)
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657 use crate::config::MarkdownFlavor;
658
659 fn check(content: &str) -> Vec<LintWarning> {
660 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
661 let rule = MD077ListContinuationIndent;
662 rule.check(&ctx).unwrap()
663 }
664
665 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
666 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
667 let rule = MD077ListContinuationIndent;
668 rule.check(&ctx).unwrap()
669 }
670
671 fn fix(content: &str) -> String {
672 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
673 let rule = MD077ListContinuationIndent;
674 rule.fix(&ctx).unwrap()
675 }
676
677 fn fix_mkdocs(content: &str) -> String {
678 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
679 let rule = MD077ListContinuationIndent;
680 rule.fix(&ctx).unwrap()
681 }
682
683 #[test]
686 fn tight_lazy_continuation_zero_indent_not_flagged() {
687 let content = "- Item\ncontinuation\n";
689 assert!(check(content).is_empty());
690 }
691
692 #[test]
693 fn tight_continuation_correct_indent_not_flagged() {
694 let content = "1. Item\n continuation\n";
696 assert!(check(content).is_empty());
697 }
698
699 #[test]
700 fn tight_continuation_over_indented_ordered() {
701 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
703 let warnings = check(content);
704 assert_eq!(warnings.len(), 1);
705 assert_eq!(warnings[0].line, 2);
706 assert!(warnings[0].message.contains("over-indented"));
707 }
708
709 #[test]
710 fn tight_continuation_over_indented_unordered() {
711 let content = "- Item\n over-indented\n";
713 let warnings = check(content);
714 assert_eq!(warnings.len(), 1);
715 assert_eq!(warnings[0].line, 2);
716 }
717
718 #[test]
719 fn tight_continuation_multiple_over_indented_lines() {
720 let content = "1. Item\n line one\n line two\n line three\n";
721 let warnings = check(content);
722 assert_eq!(warnings.len(), 3);
723 }
724
725 #[test]
726 fn tight_continuation_mixed_correct_and_over() {
727 let content = "1. Item\n correct\n over-indented\n correct again\n";
728 let warnings = check(content);
729 assert_eq!(warnings.len(), 1);
730 assert_eq!(warnings[0].line, 3);
731 }
732
733 #[test]
734 fn tight_continuation_nested_over_indented() {
735 let content = "- L1\n - L2\n over-indented continuation of L2\n";
737 let warnings = check(content);
738 assert_eq!(warnings.len(), 1);
739 assert_eq!(warnings[0].line, 3);
740 assert!(warnings[0].message.contains("expected 4"));
742 assert!(warnings[0].message.contains("found 5"));
743 }
744
745 #[test]
746 fn tight_continuation_nested_correct_indent_not_flagged() {
747 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
750 assert!(check(content).is_empty());
751 }
752
753 #[test]
754 fn fix_tight_continuation_nested_over_indented() {
755 let content = "- L1\n - L2\n over-indented continuation of L2\n";
757 let fixed = fix(content);
758 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
759 }
760
761 #[test]
762 fn tight_continuation_under_indented_not_flagged() {
763 let content = "1. Item\n under-indented\n";
766 assert!(check(content).is_empty());
767 }
768
769 #[test]
770 fn tight_continuation_tab_over_indented() {
771 let content = "- Item\n\tover-indented\n";
773 let warnings = check(content);
774 assert_eq!(warnings.len(), 1);
775 }
776
777 #[test]
778 fn fix_tight_continuation_over_indented_ordered() {
779 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
780 let fixed = fix(content);
781 assert_eq!(
782 fixed,
783 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
784 );
785 }
786
787 #[test]
788 fn fix_tight_continuation_over_indented_unordered() {
789 let content = "- Item\n over-indented\n";
790 let fixed = fix(content);
791 assert_eq!(fixed, "- Item\n over-indented\n");
792 }
793
794 #[test]
795 fn fix_tight_continuation_multiple_lines() {
796 let content = "1. Item\n line one\n line two\n";
797 let fixed = fix(content);
798 assert_eq!(fixed, "1. Item\n line one\n line two\n");
799 }
800
801 #[test]
802 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
803 let content = "1. Item\n continuation\n";
806 assert!(check_mkdocs(content).is_empty());
807 }
808
809 #[test]
810 fn tight_continuation_mkdocs_5space_ordered_flagged() {
811 let content = "1. Item\n over-indented\n";
813 let warnings = check_mkdocs(content);
814 assert_eq!(warnings.len(), 1);
815 assert!(warnings[0].message.contains("expected 4"));
816 assert!(warnings[0].message.contains("found 5"));
817 }
818
819 #[test]
820 fn fix_tight_continuation_mkdocs_over_indented() {
821 let content = "1. Item\n over-indented\n";
822 let fixed = fix_mkdocs(content);
823 assert_eq!(fixed, "1. Item\n over-indented\n");
824 }
825
826 #[test]
827 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
828 let content = "* Level 0\n * Level 1\n * Level 2\n";
831 assert!(check(content).is_empty());
832 }
833
834 #[test]
835 fn tight_continuation_ordered_marker_not_flagged() {
836 let content = "- Parent\n 1. Child item\n";
838 assert!(check(content).is_empty());
839 }
840
841 #[test]
844 fn unordered_correct_indent_no_warning() {
845 let content = "- Item\n\n continuation\n";
846 assert!(check(content).is_empty());
847 }
848
849 #[test]
850 fn unordered_partial_indent_warns() {
851 let content = "- Item\n\n continuation\n";
854 let warnings = check(content);
855 assert_eq!(warnings.len(), 1);
856 assert_eq!(warnings[0].line, 3);
857 assert!(warnings[0].message.contains("2 spaces"));
858 assert!(warnings[0].message.contains("found 1"));
859 }
860
861 #[test]
862 fn unordered_zero_indent_is_new_paragraph() {
863 let content = "- Item\n\ncontinuation\n";
866 assert!(check(content).is_empty());
867 }
868
869 #[test]
872 fn ordered_3space_correct_commonmark() {
873 let content = "1. Item\n\n continuation\n";
875 assert!(check(content).is_empty());
876 }
877
878 #[test]
879 fn ordered_2space_under_indent_commonmark() {
880 let content = "1. Item\n\n continuation\n";
881 let warnings = check(content);
882 assert_eq!(warnings.len(), 1);
883 assert!(warnings[0].message.contains("3 spaces"));
884 assert!(warnings[0].message.contains("found 2"));
885 }
886
887 #[test]
890 fn multi_digit_marker_correct() {
891 let content = "10. Item\n\n continuation\n";
893 assert!(check(content).is_empty());
894 }
895
896 #[test]
897 fn multi_digit_marker_under_indent() {
898 let content = "10. Item\n\n continuation\n";
899 let warnings = check(content);
900 assert_eq!(warnings.len(), 1);
901 assert!(warnings[0].message.contains("4 spaces"));
902 }
903
904 #[test]
907 fn mkdocs_3space_ordered_warns() {
908 let content = "1. Item\n\n continuation\n";
910 let warnings = check_mkdocs(content);
911 assert_eq!(warnings.len(), 1);
912 assert!(warnings[0].message.contains("4 spaces"));
913 assert!(warnings[0].message.contains("MkDocs"));
914 }
915
916 #[test]
917 fn mkdocs_4space_ordered_no_warning() {
918 let content = "1. Item\n\n continuation\n";
919 assert!(check_mkdocs(content).is_empty());
920 }
921
922 #[test]
923 fn mkdocs_unordered_2space_ok() {
924 let content = "- Item\n\n continuation\n";
926 assert!(check_mkdocs(content).is_empty());
927 }
928
929 #[test]
930 fn mkdocs_unordered_2space_warns() {
931 let content = "- Item\n\n continuation\n";
933 let warnings = check_mkdocs(content);
934 assert_eq!(warnings.len(), 1);
935 assert!(warnings[0].message.contains("4 spaces"));
936 }
937
938 #[test]
941 fn fix_unordered_indent() {
942 let content = "- Item\n\n continuation\n";
944 let fixed = fix(content);
945 assert_eq!(fixed, "- Item\n\n continuation\n");
946 }
947
948 #[test]
949 fn fix_ordered_indent() {
950 let content = "1. Item\n\n continuation\n";
951 let fixed = fix(content);
952 assert_eq!(fixed, "1. Item\n\n continuation\n");
953 }
954
955 #[test]
956 fn fix_mkdocs_indent() {
957 let content = "1. Item\n\n continuation\n";
958 let fixed = fix_mkdocs(content);
959 assert_eq!(fixed, "1. Item\n\n continuation\n");
960 }
961
962 #[test]
965 fn nested_list_items_not_flagged() {
966 let content = "- Parent\n\n - Child\n";
967 assert!(check(content).is_empty());
968 }
969
970 #[test]
971 fn nested_list_zero_indent_is_new_paragraph() {
972 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
974 assert!(check(content).is_empty());
975 }
976
977 #[test]
978 fn nested_list_partial_indent_flagged() {
979 let content = "- Parent\n - Child\n\n continuation of parent\n";
981 let warnings = check(content);
982 assert_eq!(warnings.len(), 1);
983 assert!(warnings[0].message.contains("2 spaces"));
984 }
985
986 #[test]
989 fn code_block_correctly_indented_no_warning() {
990 let content = "- Item\n\n ```\n code\n ```\n";
992 assert!(check(content).is_empty());
993 }
994
995 #[test]
996 fn code_fence_under_indented_warns() {
997 let content = "- Item\n\n ```\n code\n ```\n";
1001 let warnings = check(content);
1002 assert_eq!(warnings.len(), 1);
1003 assert_eq!(warnings[0].line, 3);
1004 }
1005
1006 #[test]
1007 fn code_fence_under_indented_ordered_mkdocs() {
1008 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
1011 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
1013 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1015 assert!(warnings[0].message.contains("4 spaces"));
1016 assert!(warnings[0].message.contains("MkDocs"));
1017 }
1018
1019 #[test]
1020 fn code_fence_tilde_under_indented() {
1021 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1022 let warnings = check(content);
1023 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1025 }
1026
1027 #[test]
1030 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
1031 let content = "- Item\n\n\ncontinuation\n";
1033 assert!(check(content).is_empty());
1034 }
1035
1036 #[test]
1037 fn multiple_blank_lines_partial_indent_flags() {
1038 let content = "- Item\n\n\n continuation\n";
1039 let warnings = check(content);
1040 assert_eq!(warnings.len(), 1);
1041 }
1042
1043 #[test]
1046 fn empty_item_no_warning() {
1047 let content = "- \n- Second\n";
1048 assert!(check(content).is_empty());
1049 }
1050
1051 #[test]
1054 fn multiple_items_mixed_indent() {
1055 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
1056 let warnings = check(content);
1057 assert_eq!(warnings.len(), 1);
1058 assert_eq!(warnings[0].line, 7);
1059 }
1060
1061 #[test]
1064 fn task_list_correct_indent() {
1065 let content = "- [ ] Task\n\n continuation\n";
1067 assert!(check(content).is_empty());
1068 }
1069
1070 #[test]
1073 fn frontmatter_not_flagged() {
1074 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
1075 assert!(check(content).is_empty());
1076 }
1077
1078 #[test]
1081 fn fix_multiple_items() {
1082 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
1083 let fixed = fix(content);
1084 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
1085 }
1086
1087 #[test]
1088 fn fix_multiline_loose_continuation_all_lines() {
1089 let content = "1. Item\n\n line one\n line two\n line three\n";
1090 let fixed = fix(content);
1091 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
1092 }
1093
1094 #[test]
1097 fn sibling_item_boundary_respected() {
1098 let content = "- First\n- Second\n\n continuation\n";
1100 assert!(check(content).is_empty());
1101 }
1102
1103 #[test]
1106 fn blockquote_list_correct_indent_no_warning() {
1107 let content = "> - Item\n>\n> continuation\n";
1110 assert!(check(content).is_empty());
1111 }
1112
1113 #[test]
1114 fn blockquote_list_under_indent_no_false_positive() {
1115 let content = "> - Item\n>\n> continuation\n";
1120 assert!(check(content).is_empty());
1121 }
1122
1123 #[test]
1126 fn deeply_nested_correct_indent() {
1127 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1128 assert!(check(content).is_empty());
1129 }
1130
1131 #[test]
1132 fn deeply_nested_under_indent() {
1133 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1136 let warnings = check(content);
1137 assert_eq!(warnings.len(), 1);
1138 assert!(warnings[0].message.contains("6 spaces"));
1139 assert!(warnings[0].message.contains("found 5"));
1140 }
1141
1142 #[test]
1145 fn loose_tab_continuation_over_indented() {
1146 let content = "- Item\n\n\tcontinuation\n";
1151 let warnings = check(content);
1152 assert_eq!(warnings.len(), 1);
1153 assert_eq!(warnings[0].line, 3);
1154 assert_eq!(fix(content), "- Item\n\n continuation\n");
1155 }
1156
1157 #[test]
1160 fn multiple_continuations_correct() {
1161 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
1162 assert!(check(content).is_empty());
1163 }
1164
1165 #[test]
1166 fn multiple_continuations_second_under_indent() {
1167 let content = "- Item\n\n para 1\n\n continuation 2\n";
1169 let warnings = check(content);
1170 assert_eq!(warnings.len(), 1);
1171 assert_eq!(warnings[0].line, 5);
1172 }
1173
1174 #[test]
1177 fn ordered_paren_marker_correct() {
1178 let content = "1) Item\n\n continuation\n";
1180 assert!(check(content).is_empty());
1181 }
1182
1183 #[test]
1184 fn ordered_paren_marker_under_indent() {
1185 let content = "1) Item\n\n continuation\n";
1186 let warnings = check(content);
1187 assert_eq!(warnings.len(), 1);
1188 assert!(warnings[0].message.contains("3 spaces"));
1189 }
1190
1191 #[test]
1194 fn star_marker_correct() {
1195 let content = "* Item\n\n continuation\n";
1196 assert!(check(content).is_empty());
1197 }
1198
1199 #[test]
1200 fn star_marker_under_indent() {
1201 let content = "* Item\n\n continuation\n";
1202 let warnings = check(content);
1203 assert_eq!(warnings.len(), 1);
1204 }
1205
1206 #[test]
1207 fn plus_marker_correct() {
1208 let content = "+ Item\n\n continuation\n";
1209 assert!(check(content).is_empty());
1210 }
1211
1212 #[test]
1215 fn heading_after_list_no_warning() {
1216 let content = "- Item\n\n# Heading\n";
1217 assert!(check(content).is_empty());
1218 }
1219
1220 #[test]
1223 fn hr_after_list_no_warning() {
1224 let content = "- Item\n\n---\n";
1225 assert!(check(content).is_empty());
1226 }
1227
1228 #[test]
1231 fn reference_link_def_not_flagged() {
1232 let content = "- Item\n\n [link]: https://example.com\n";
1233 assert!(check(content).is_empty());
1234 }
1235
1236 #[test]
1239 fn footnote_def_not_flagged() {
1240 let content = "- Item\n\n [^1]: footnote text\n";
1241 assert!(check(content).is_empty());
1242 }
1243
1244 #[test]
1245 fn footnote_multiline_body_after_list_not_flagged() {
1246 let content = "# A list followed by a footnote\n\n\
1250 Here is a paragraph.[^fn]\n\n\
1251 - This is a list.\n\n\
1252 [^fn]:\n\
1253 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1254 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1255 assert!(check(content).is_empty());
1256 }
1257
1258 #[test]
1259 fn fix_footnote_multiline_body_after_list_is_noop() {
1260 let content = "# A list followed by a footnote\n\n\
1264 Here is a paragraph.[^fn]\n\n\
1265 - This is a list.\n\n\
1266 [^fn]:\n\
1267 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1268 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1269 assert_eq!(fix(content), content);
1270 }
1271
1272 #[test]
1273 fn footnote_body_indented_past_list_content_col_not_flagged() {
1274 let content = "- Item\n\n[^fn]:\n Body line one.\n Body line two.\n";
1278 assert!(check(content).is_empty());
1279 }
1280
1281 #[test]
1282 fn list_inside_footnote_body_continuation_not_flagged() {
1283 let content = "Text.[^fn]\n\n[^fn]:\n\
1287 \x20\x20\x20\x20- nested item\n\
1288 \x20\x20\x20\x20\x20\x20\x20over-indented continuation\n";
1289 assert!(check(content).is_empty());
1290 }
1291
1292 #[test]
1293 fn footnote_multiline_body_after_list_not_flagged_mkdocs() {
1294 let content = "Here is a paragraph.[^fn]\n\n\
1298 - This is a list.\n\n\
1299 [^fn]:\n\
1300 \x20\x20\x20\x20\x20\x20Footnote body that spans\n\
1301 \x20\x20\x20\x20\x20\x20multiple indented lines.\n";
1302 assert!(check_mkdocs(content).is_empty());
1303 }
1304
1305 #[test]
1308 fn fix_deeply_nested() {
1309 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
1310 let fixed = fix(content);
1311 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
1312 }
1313
1314 #[test]
1315 fn fix_mkdocs_unordered() {
1316 let content = "- Item\n\n continuation\n";
1318 let fixed = fix_mkdocs(content);
1319 assert_eq!(fixed, "- Item\n\n continuation\n");
1320 }
1321
1322 #[test]
1323 fn fix_code_fence_indent() {
1324 let content = "- Item\n\n ```\n code\n ```\n";
1327 let fixed = fix(content);
1328 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
1329 }
1330
1331 #[test]
1332 fn fix_mkdocs_code_fence_indent() {
1333 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
1335 let fixed = fix_mkdocs(content);
1336 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
1337 }
1338
1339 #[test]
1342 fn empty_document_no_warning() {
1343 assert!(check("").is_empty());
1344 }
1345
1346 #[test]
1347 fn whitespace_only_no_warning() {
1348 assert!(check(" \n\n \n").is_empty());
1349 }
1350
1351 #[test]
1354 fn no_list_no_warning() {
1355 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
1356 assert!(check(content).is_empty());
1357 }
1358
1359 #[test]
1362 fn multiline_continuation_all_lines_flagged() {
1363 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";
1364 let warnings = check(content);
1365 assert_eq!(warnings.len(), 3);
1366 assert_eq!(warnings[0].line, 3);
1367 assert_eq!(warnings[1].line, 4);
1368 assert_eq!(warnings[2].line, 5);
1369 }
1370
1371 #[test]
1372 fn multiline_continuation_with_frontmatter_fix() {
1373 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";
1374 let fixed = fix(content);
1375 assert_eq!(
1376 fixed,
1377 "---\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"
1378 );
1379 }
1380
1381 #[test]
1382 fn multiline_continuation_correct_indent_no_warning() {
1383 let content = "1. Item\n\n line one\n line two\n line three\n";
1384 assert!(check(content).is_empty());
1385 }
1386
1387 #[test]
1388 fn multiline_continuation_mixed_indent() {
1389 let content = "1. Item\n\n correct\n wrong\n correct\n";
1390 let warnings = check(content);
1391 assert_eq!(warnings.len(), 1);
1392 assert_eq!(warnings[0].line, 4);
1393 }
1394
1395 #[test]
1396 fn multiline_continuation_unordered() {
1397 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
1398 let warnings = check(content);
1399 assert_eq!(warnings.len(), 3);
1400 let fixed = fix(content);
1401 assert_eq!(
1402 fixed,
1403 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
1404 );
1405 }
1406
1407 #[test]
1408 fn multiline_continuation_two_items_fix() {
1409 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
1410 let fixed = fix(content);
1411 assert_eq!(
1412 fixed,
1413 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1414 );
1415 }
1416
1417 #[test]
1418 fn fence_fix_does_not_break_pairing_for_md031() {
1419 let content = "#### title\n\nabc\n\n\
1426 1. ab\n\n\
1427 \x20\x20`aabbccdd`\n\n\
1428 2. cd\n\n\
1429 \x20\x20`bbcc dd ee`\n\n\
1430 \x20\x20```\n\
1431 \x20\x20abcd\n\
1432 \x20\x20ef gh\n\
1433 \x20\x20```\n\n\
1434 \x20\x20uu\n\n\
1435 \x20\x20```\n\
1436 \x20\x20cdef\n\
1437 \x20\x20gh ij\n\
1438 \x20\x20```\n";
1439 let expected = "#### title\n\nabc\n\n\
1440 1. ab\n\n\
1441 \x20\x20\x20`aabbccdd`\n\n\
1442 2. cd\n\n\
1443 \x20\x20\x20`bbcc dd ee`\n\n\
1444 \x20\x20\x20```\n\
1445 \x20\x20\x20abcd\n\
1446 \x20\x20\x20ef gh\n\
1447 \x20\x20\x20```\n\n\
1448 \x20\x20\x20uu\n\n\
1449 \x20\x20\x20```\n\
1450 \x20\x20\x20cdef\n\
1451 \x20\x20\x20gh ij\n\
1452 \x20\x20\x20```\n";
1453 assert_eq!(fix(content), expected);
1454 }
1455
1456 #[test]
1457 fn multiline_continuation_separated_by_blank() {
1458 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1459 let warnings = check(content);
1460 assert_eq!(warnings.len(), 4);
1461 let fixed = fix(content);
1462 assert_eq!(
1463 fixed,
1464 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1465 );
1466 }
1467
1468 #[test]
1469 fn tab_indented_fence_is_normalized_to_spaces() {
1470 let content = "100. ab\n\n\t```\n\tabcd\n\t```\n";
1478 let expected = "100. ab\n\n ```\n abcd\n ```\n";
1479 assert_eq!(fix(content), expected);
1480 }
1481
1482 #[test]
1491 fn loose_continuation_over_indented_flagged() {
1492 let content = "* Item\n\n over-indented\n";
1495 let warnings = check(content);
1496 assert_eq!(warnings.len(), 1);
1497 assert_eq!(warnings[0].line, 3);
1498 assert!(warnings[0].message.contains("over-indented"));
1499 assert!(warnings[0].message.contains("expected 2"));
1500 assert!(warnings[0].message.contains("found 3"));
1501 }
1502
1503 #[test]
1504 fn loose_continuation_over_indented_multiline_mixed() {
1505 let content = "* Item\n\n over one\n correct\n over two\n";
1507 let warnings = check(content);
1508 assert_eq!(warnings.len(), 2);
1509 assert_eq!(warnings[0].line, 3);
1510 assert_eq!(warnings[1].line, 5);
1511 }
1512
1513 #[test]
1514 fn fix_loose_continuation_over_indented() {
1515 let content = "* Item\n\n over one\n correct\n over two\n";
1516 let fixed = fix(content);
1517 assert_eq!(fixed, "* Item\n\n over one\n correct\n over two\n");
1518 }
1519
1520 #[test]
1521 fn fix_tight_and_loose_items_normalized_identically() {
1522 let content = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1525 * 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\
1526 * 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";
1527 let expected = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1528 * 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\
1529 * 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";
1530 assert_eq!(fix(content), expected);
1531 }
1532
1533 #[test]
1534 fn multi_paragraph_item_loose_paragraph_over_indented() {
1535 let content = "* Item.\n tight over\n\n loose over\n";
1538 let warnings = check(content);
1539 assert_eq!(warnings.len(), 2);
1540 assert_eq!(warnings[0].line, 2);
1541 assert_eq!(warnings[1].line, 4);
1542 }
1543
1544 #[test]
1545 fn loose_indented_code_block_not_flagged() {
1546 let content = "- Item\n\n code line\n";
1550 assert!(check(content).is_empty());
1551 }
1552
1553 #[test]
1554 fn mkdocs_loose_over_indented_flagged() {
1555 let content = "1. Item\n\n over\n";
1558 let warnings = check_mkdocs(content);
1559 assert_eq!(warnings.len(), 1);
1560 assert_eq!(warnings[0].line, 3);
1561 assert!(warnings[0].message.contains("over-indented"));
1562 assert!(warnings[0].message.contains("expected 4"));
1563 assert!(warnings[0].message.contains("found 5"));
1564 }
1565
1566 #[test]
1567 fn task_list_loose_over_indented_flagged() {
1568 let content = "- [ ] Task\n\n over\n";
1571 let warnings = check(content);
1572 assert_eq!(warnings.len(), 1);
1573 assert_eq!(warnings[0].line, 3);
1574 }
1575
1576 #[test]
1577 fn loose_over_indent_boundary_below_code_block_threshold_flagged() {
1578 let content = "- Item\n\n over\n";
1583 let warnings = check(content);
1584 assert_eq!(warnings.len(), 1);
1585 assert_eq!(warnings[0].line, 3);
1586 assert!(warnings[0].message.contains("expected 2"));
1587 assert!(warnings[0].message.contains("found 5"));
1588 }
1589
1590 #[test]
1591 fn loose_over_indent_does_not_steal_nested_under_indent() {
1592 let content = "- Outer\n - Inner\n\n continuation\n";
1599 let warnings = check(content);
1600 assert_eq!(warnings.len(), 1);
1601 assert_eq!(warnings[0].line, 4);
1602 assert!(warnings[0].message.contains("4 spaces"));
1603 assert!(warnings[0].message.contains("found 3"));
1604 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1605 }
1606
1607 #[test]
1608 fn loose_over_indent_attributes_to_deepest_enclosing_item() {
1609 let content = "- Outer\n - Inner\n\n continuation\n";
1613 let warnings = check(content);
1614 assert_eq!(warnings.len(), 1);
1615 assert_eq!(warnings[0].line, 4);
1616 assert!(warnings[0].message.contains("expected 4"));
1617 assert!(warnings[0].message.contains("found 5"));
1618 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1619 }
1620
1621 #[test]
1630 fn loose_over_indented_fence_not_flagged() {
1631 let content = "- Item\n\n ```\n code\n ```\n";
1632 assert!(check(content).is_empty());
1633 assert_eq!(fix(content), content);
1634 }
1635
1636 #[test]
1637 fn tight_over_indented_fence_not_flagged() {
1638 let content = "- Item\n ```\n code\n ```\n";
1639 assert!(check(content).is_empty());
1640 assert_eq!(fix(content), content);
1641 }
1642
1643 #[test]
1644 fn over_indented_tilde_fence_not_flagged() {
1645 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1646 assert!(check(content).is_empty());
1647 assert_eq!(fix(content), content);
1648 }
1649
1650 #[test]
1651 fn fence_like_code_content_inside_fenced_block_not_flagged() {
1652 let content = "- Item\n\n ~~~\n ```\n ~~~\n";
1655 assert!(check(content).is_empty());
1656 assert_eq!(fix(content), content);
1657 }
1658
1659 #[test]
1660 fn unterminated_over_indented_fence_not_flagged() {
1661 let content = "- Item\n\n ```\n code1\n code2deeper\n";
1664 assert!(check(content).is_empty());
1665 assert_eq!(fix(content), content);
1666 }
1667
1668 #[test]
1676 fn task_list_tight_continuation_post_checkbox_reproducer_579() {
1677 let content = "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n";
1680 assert!(check(content).is_empty());
1681 }
1682
1683 #[test]
1684 fn task_list_tight_continuation_dash_unchecked() {
1685 let content = "- [ ] Task\n continuation\n";
1686 assert!(check(content).is_empty());
1687 }
1688
1689 #[test]
1690 fn task_list_tight_continuation_dash_checked_lower() {
1691 let content = "- [x] Task\n continuation\n";
1692 assert!(check(content).is_empty());
1693 }
1694
1695 #[test]
1696 fn task_list_tight_continuation_dash_checked_upper() {
1697 let content = "- [X] Task\n continuation\n";
1698 assert!(check(content).is_empty());
1699 }
1700
1701 #[test]
1702 fn task_list_tight_continuation_star_marker() {
1703 let content = "* [ ] Task\n continuation\n";
1704 assert!(check(content).is_empty());
1705 }
1706
1707 #[test]
1708 fn task_list_tight_continuation_plus_marker() {
1709 let content = "+ [ ] Task\n continuation\n";
1710 assert!(check(content).is_empty());
1711 }
1712
1713 #[test]
1714 fn task_list_tight_continuation_content_column_still_valid() {
1715 let content = "- [ ] Task\n continuation\n";
1718 assert!(check(content).is_empty());
1719 }
1720
1721 #[test]
1722 fn task_list_tight_continuation_between_columns_still_flagged() {
1723 let content = "- [ ] Task\n continuation\n";
1726 let warnings = check(content);
1727 assert_eq!(warnings.len(), 1);
1728 assert!(warnings[0].message.contains("expected 2 or 6"));
1730 assert!(warnings[0].message.contains("found 4"));
1731 }
1732
1733 #[test]
1734 fn task_list_tight_continuation_overshoot_still_flagged() {
1735 let content = "- [ ] Task\n continuation\n";
1737 let warnings = check(content);
1738 assert_eq!(warnings.len(), 1);
1739 assert!(warnings[0].message.contains("expected 2 or 6"));
1740 assert!(warnings[0].message.contains("found 7"));
1741 }
1742
1743 #[test]
1746 fn fix_task_list_overshoot_snaps_to_task_col() {
1747 let content = "- [ ] Task\n continuation\n";
1751 let fixed = fix(content);
1752 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1753 }
1754
1755 #[test]
1756 fn fix_task_list_col_5_snaps_to_task_col() {
1757 let content = "- [ ] Task\n continuation\n";
1759 let fixed = fix(content);
1760 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1761 }
1762
1763 #[test]
1764 fn fix_task_list_col_3_snaps_to_content_col() {
1765 let content = "- [ ] Task\n continuation\n";
1767 let fixed = fix(content);
1768 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1769 }
1770
1771 #[test]
1772 fn fix_task_list_col_4_ties_to_content_col() {
1773 let content = "- [ ] Task\n continuation\n";
1778 let fixed = fix(content);
1779 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1780 }
1781
1782 #[test]
1783 fn fix_task_list_ordered_overshoot_snaps_to_task_col() {
1784 let content = "1. [ ] Task\n continuation\n";
1787 let fixed = fix(content);
1788 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1789 }
1790
1791 #[test]
1792 fn fix_task_list_ordered_under_overshoot_snaps_to_content_col() {
1793 let content = "1. [ ] Task\n continuation\n";
1796 let fixed = fix(content);
1797 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1798 }
1799
1800 #[test]
1801 fn task_list_tight_continuation_ordered_single_digit() {
1802 let content = "1. [ ] Task\n continuation\n";
1804 assert!(check(content).is_empty());
1805 }
1806
1807 #[test]
1808 fn task_list_tight_continuation_ordered_multi_digit() {
1809 let content = "10. [ ] Task\n continuation\n";
1811 assert!(check(content).is_empty());
1812 }
1813
1814 #[test]
1815 fn task_list_tight_continuation_nested_dash() {
1816 let content = "- Parent\n - [ ] Nested task\n continuation\n";
1818 assert!(check(content).is_empty());
1819 }
1820
1821 #[test]
1822 fn task_list_loose_continuation_post_checkbox_column_not_flagged() {
1823 let content = "- [ ] Task\n\n continuation\n";
1828 assert!(check(content).is_empty());
1829 }
1830
1831 #[test]
1832 fn task_list_empty_body_is_not_a_task() {
1833 let content = "- [ ]\n continuation\n";
1839 let warnings = check(content);
1840 assert_eq!(warnings.len(), 1);
1841 assert!(warnings[0].message.contains("found 4"));
1842 }
1843
1844 #[test]
1845 fn task_list_malformed_checkbox_is_not_a_task() {
1846 let content = "- [~] Not a task\n continuation\n";
1848 let warnings = check(content);
1849 assert_eq!(warnings.len(), 1);
1850 }
1851
1852 #[test]
1859 fn task_list_mkdocs_unordered_required_min_valid() {
1860 let content = "- [ ] Task\n continuation\n";
1862 assert!(check_mkdocs(content).is_empty());
1863 }
1864
1865 #[test]
1866 fn task_list_mkdocs_unordered_post_checkbox_valid() {
1867 let content = "- [ ] Task\n continuation\n";
1868 assert!(check_mkdocs(content).is_empty());
1869 }
1870
1871 #[test]
1872 fn task_list_mkdocs_unordered_between_flagged() {
1873 let content = "- [ ] Task\n continuation\n";
1875 let warnings = check_mkdocs(content);
1876 assert_eq!(warnings.len(), 1);
1877 }
1878
1879 #[test]
1880 fn task_list_mkdocs_ordered_both_columns_valid() {
1881 let at_4 = "1. [ ] Task\n continuation\n";
1883 assert!(check_mkdocs(at_4).is_empty());
1884 let at_7 = "1. [ ] Task\n continuation\n";
1885 assert!(check_mkdocs(at_7).is_empty());
1886 }
1887
1888 #[test]
1889 fn task_list_mkdocs_ordered_between_flagged() {
1890 let at_5 = "1. [ ] Task\n continuation\n";
1892 assert_eq!(check_mkdocs(at_5).len(), 1);
1893 let at_6 = "1. [ ] Task\n continuation\n";
1894 assert_eq!(check_mkdocs(at_6).len(), 1);
1895 }
1896
1897 #[test]
1907 fn fix_task_list_tie_sibling_at_task_col_snaps_to_task_col() {
1908 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
1912 let fixed = fix(content);
1913 assert_eq!(
1914 fixed,
1915 "- [ ] Task\n aligned continuation\n tied continuation\n"
1916 );
1917 }
1918
1919 #[test]
1920 fn fix_task_list_tie_sibling_at_content_col_snaps_to_content_col() {
1921 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
1924 let fixed = fix(content);
1925 assert_eq!(fixed, "- [ ] Task\n aligned continuation\n tied continuation\n");
1926 }
1927
1928 #[test]
1929 fn fix_task_list_tie_both_siblings_snaps_to_content_col() {
1930 let content = "- [ ] Task\n at content col\n at task col\n tied continuation\n";
1934 let fixed = fix(content);
1935 assert_eq!(
1936 fixed,
1937 "- [ ] Task\n at content col\n at task col\n tied continuation\n"
1938 );
1939 }
1940
1941 #[test]
1942 fn fix_task_list_tie_sees_task_col_through_tight_lazy_continuation() {
1943 let content = concat!("- [ ] Task\n", "lazy\n", " aligned at task col\n", " tied\n",);
1957 let fixed = fix(content);
1958 assert!(
1959 fixed.contains("\n tied\n"),
1960 "tied line should snap to col 6 (task col) because a task-col \
1961 sibling is visible past the tight lazy-continuation line; got:\n{fixed}"
1962 );
1963 }
1964
1965 #[test]
1972 fn task_list_tab_indented_continuation_flagged() {
1973 let content = "- [ ] Task\n\t\twrap\n";
1976 let warnings = check(content);
1977 assert_eq!(warnings.len(), 1);
1978 assert!(warnings[0].message.contains("expected 2 or 6"));
1979 assert!(warnings[0].message.contains("found 8"));
1980 }
1981
1982 #[test]
1983 fn fix_task_list_tab_indented_snaps_to_task_col() {
1984 let content = "- [ ] Task\n\t\twrap\n";
1986 let fixed = fix(content);
1987 assert_eq!(fixed, "- [ ] Task\n wrap\n");
1988 }
1989
1990 #[test]
1991 fn fix_task_list_single_tab_equidistant_snaps_to_content_col() {
1992 let content = "- [ ] Task\n\twrap\n";
1995 let fixed = fix(content);
1996 assert_eq!(fixed, "- [ ] Task\n wrap\n");
1997 }
1998
1999 #[test]
2009 fn task_list_blockquote_post_checkbox_not_flagged() {
2010 let content = "> - [ ] Task\n> continuation\n";
2012 assert!(check(content).is_empty());
2013 }
2014
2015 #[test]
2016 fn task_list_blockquote_between_cols_documented_limitation() {
2017 let content = "> - [ ] Task\n> continuation\n";
2021 assert!(check(content).is_empty());
2022 }
2023
2024 #[test]
2025 fn task_list_blockquote_overshoot_documented_limitation() {
2026 let content = "> - [ ] Task\n> continuation\n";
2028 assert!(check(content).is_empty());
2029 }
2030
2031 #[test]
2038 fn fix_task_list_mkdocs_unordered_overshoot_snaps_to_task_col() {
2039 let content = "- [ ] Task\n continuation\n";
2042 let fixed = fix_mkdocs(content);
2043 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2044 }
2045
2046 #[test]
2047 fn fix_task_list_mkdocs_unordered_tie_snaps_to_required() {
2048 let content = "- [ ] Task\n continuation\n";
2051 let fixed = fix_mkdocs(content);
2052 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2053 }
2054
2055 #[test]
2056 fn fix_task_list_mkdocs_ordered_overshoot_snaps_to_task_col() {
2057 let content = "1. [ ] Task\n continuation\n";
2060 let fixed = fix_mkdocs(content);
2061 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2062 }
2063
2064 #[test]
2065 fn fix_task_list_mkdocs_ordered_near_required_snaps_to_required() {
2066 let content = "1. [ ] Task\n continuation\n";
2072 let fixed = fix_mkdocs(content);
2073 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2074 }
2075
2076 #[test]
2077 fn fix_task_list_mkdocs_ordered_between_cols_snaps_to_task_col() {
2078 let content = "1. [ ] Task\n continuation\n";
2081 let fixed = fix_mkdocs(content);
2082 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2083 }
2084
2085 fn assert_idempotent(content: &str) {
2095 let once = fix(content);
2096 let twice = fix(&once);
2097 assert_eq!(once, twice, "MD077 fix was not idempotent on input: {content:?}");
2098 }
2099
2100 fn assert_idempotent_mkdocs(content: &str) {
2101 let once = fix_mkdocs(content);
2102 let twice = fix_mkdocs(&once);
2103 assert_eq!(
2104 once, twice,
2105 "MD077 (MkDocs) fix was not idempotent on input: {content:?}"
2106 );
2107 }
2108
2109 #[test]
2110 fn idempotent_task_list_between_cols() {
2111 assert_idempotent("- [ ] Task\n continuation\n");
2112 }
2113
2114 #[test]
2115 fn idempotent_task_list_overshoot() {
2116 assert_idempotent("- [ ] Task\n continuation\n");
2117 }
2118
2119 #[test]
2120 fn idempotent_task_list_under_post_checkbox() {
2121 assert_idempotent("- [ ] Task\n continuation\n");
2122 }
2123
2124 #[test]
2125 fn idempotent_task_list_near_post_checkbox() {
2126 assert_idempotent("- [ ] Task\n continuation\n");
2127 }
2128
2129 #[test]
2130 fn idempotent_task_list_tab_overshoot() {
2131 assert_idempotent("- [ ] Task\n\t\twrap\n");
2132 }
2133
2134 #[test]
2135 fn idempotent_task_list_single_tab() {
2136 assert_idempotent("- [ ] Task\n\twrap\n");
2137 }
2138
2139 #[test]
2140 fn idempotent_task_list_ordered_overshoot() {
2141 assert_idempotent("1. [ ] Task\n continuation\n");
2142 }
2143
2144 #[test]
2145 fn idempotent_task_list_ordered_under() {
2146 assert_idempotent("1. [ ] Task\n continuation\n");
2147 }
2148
2149 #[test]
2150 fn idempotent_task_list_tie_with_sibling_at_task_col() {
2151 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2152 }
2153
2154 #[test]
2155 fn idempotent_task_list_tie_with_sibling_at_content_col() {
2156 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2157 }
2158
2159 #[test]
2160 fn idempotent_task_list_mkdocs_unordered_overshoot() {
2161 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2162 }
2163
2164 #[test]
2165 fn idempotent_task_list_mkdocs_unordered_tie() {
2166 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2167 }
2168
2169 #[test]
2170 fn idempotent_task_list_mkdocs_ordered_overshoot() {
2171 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2172 }
2173
2174 #[test]
2175 fn idempotent_task_list_mkdocs_ordered_between() {
2176 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2177 }
2178
2179 #[test]
2180 fn idempotent_task_list_reproducer_579() {
2181 assert_idempotent(
2185 "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n",
2186 );
2187 }
2188
2189 #[test]
2190 fn idempotent_non_task_list_still_holds() {
2191 assert_idempotent("1. Item\n over-indented\n");
2194 assert_idempotent("- Item\n\n continuation\n");
2195 }
2196
2197 #[test]
2204 fn idempotent_non_task_loose_under_indent_ordered() {
2205 assert_idempotent("1. Item\n\n continuation\n");
2207 }
2208
2209 #[test]
2210 fn idempotent_non_task_loose_under_indent_multi_digit() {
2211 assert_idempotent("10. Item\n\n continuation\n");
2213 }
2214
2215 #[test]
2216 fn idempotent_non_task_tight_over_indent_ordered() {
2217 assert_idempotent("1. Item\n over-indented\n");
2219 }
2220
2221 #[test]
2229 fn idempotent_non_task_fence_ordered_loose() {
2230 assert_idempotent("1. Item\n\n ```rust\n let x = 1;\n ```\n");
2232 }
2233
2234 #[test]
2235 fn idempotent_non_task_fence_tilde_under_indent() {
2236 assert_idempotent("1. Item\n\n ~~~\nplain text\n ~~~\n");
2242 }
2243
2244 #[test]
2245 fn idempotent_non_task_fence_interior_above_required() {
2246 assert_idempotent("1. Item\n\n ```\n deeply indented code\n ```\n");
2250 }
2251
2252 #[test]
2253 fn fence_fix_promotes_interior_below_scope_in_single_pass() {
2254 let content = "1. Item\n\n ```\ncode\n ```\n";
2258 let fixed = fix(content);
2259 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2260 }
2261
2262 #[test]
2263 fn fence_fix_preserves_interior_above_required() {
2264 let content = "1. Item\n\n ```\n code\n ```\n";
2267 let fixed = fix(content);
2268 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2269 }
2270
2271 #[test]
2278 fn idempotent_non_task_mkdocs_ordered_at_3_spaces() {
2279 assert_idempotent_mkdocs("1. Item\n\n continuation\n");
2281 }
2282
2283 #[test]
2284 fn idempotent_non_task_mkdocs_unordered_at_2_spaces() {
2285 assert_idempotent_mkdocs("- Item\n\n continuation\n");
2287 }
2288
2289 #[test]
2290 fn idempotent_non_task_mkdocs_fence_compound() {
2291 assert_idempotent_mkdocs("1. Item\n\n ```toml\n k = 1\n ```\n");
2293 }
2294}