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.len() + 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.len() + 1, |ci| ci.content(ctx.content).len() + 1);
424 let extra_flag = (closer_line != line.line_num).then_some(closer_line);
425 (fix, closer_line, end_column, extra_flag)
426 } else {
427 let fix_start = line.info.byte_offset;
428 let fix_end = fix_start + line.info.indent;
429 let fix = Some(Fix::new(fix_start..fix_end, " ".repeat(required)));
430 (fix, line.line_num, line_content.len() + 1, None)
431 };
432
433 UnderIndentOutcome {
434 warning: LintWarning {
435 rule_name: Some("MD077".to_string()),
436 line: line.line_num,
437 column: 1,
438 end_line: warn_end_line,
439 end_column: warn_end_column,
440 message,
441 severity: Severity::Warning,
442 fix,
443 },
444 also_flag_line: compound_closer,
445 }
446 }
447}
448
449struct ContinuationLine<'a> {
453 line_num: usize,
454 info: &'a LineInfo,
455 trimmed: &'a str,
456 actual: usize,
457 saw_blank: bool,
458}
459
460struct UnderIndentOutcome {
465 warning: LintWarning,
466 also_flag_line: Option<usize>,
467}
468
469impl Rule for MD077ListContinuationIndent {
470 fn name(&self) -> &'static str {
471 "MD077"
472 }
473
474 fn description(&self) -> &'static str {
475 "List continuation content indentation"
476 }
477
478 fn check(&self, ctx: &LintContext) -> LintResult {
479 if ctx.content.is_empty() {
480 return Ok(Vec::new());
481 }
482
483 let strict_indent = ctx.flavor.requires_strict_list_indent();
484 let total_lines = ctx.lines.len();
485 let mut warnings = Vec::new();
486 let mut flagged_lines = std::collections::HashSet::new();
487
488 let mut items: Vec<(usize, usize, usize, Option<usize>)> = Vec::new();
497 for block in &ctx.list_blocks {
498 for &item_line in &block.item_lines {
499 if let Some(info) = ctx.line_info(item_line)
500 && let Some(ref li) = info.list_item
501 {
502 let line = info.content(ctx.content);
503 let task_col = Self::is_task_list_item(line, li.content_column)
504 .then_some(li.content_column + Self::TASK_CHECKBOX_PREFIX_LEN);
505 items.push((item_line, li.marker_column, li.content_column, task_col));
506 }
507 }
508 }
509 items.sort_unstable();
510 items.dedup_by_key(|&mut (ln, _, _, _)| ln);
511
512 let scoped: Vec<(usize, usize, usize, Option<usize>, usize, usize)> = items
516 .iter()
517 .enumerate()
518 .map(|(item_idx, &(item_line, marker_col, content_col, task_col))| {
519 let required = if strict_indent { content_col.max(4) } else { content_col };
520 let range_end = items
521 .iter()
522 .skip(item_idx + 1)
523 .find(|&&(_, mc, _, _)| mc <= marker_col)
524 .map_or(total_lines, |&(ln, _, _, _)| ln - 1);
525 (item_line, marker_col, content_col, task_col, required, range_end)
526 })
527 .collect();
528
529 for &(item_line, marker_col, _content_col, _task_col, required, range_end) in &scoped {
537 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
538 let actual = line.actual;
539 if line.saw_blank && actual < required && flagged_lines.insert(line.line_num) {
540 let message = if strict_indent {
541 format!(
542 "Content inside list item needs {required} spaces of indentation \
543 for MkDocs compatibility (found {actual})",
544 )
545 } else {
546 format!(
547 "Content after blank line in list item needs {required} spaces of \
548 indentation to remain part of the list (found {actual})",
549 )
550 };
551 let outcome = Self::build_under_indent_warning(ctx, line, required, message);
552 if let Some(closer_line) = outcome.also_flag_line {
553 flagged_lines.insert(closer_line);
554 }
555 warnings.push(outcome.warning);
556 }
557 ControlFlow::Continue(())
558 });
559 }
560
561 for &(item_line, marker_col, content_col, task_col, required, range_end) in &scoped {
570 let (uses_content_col, uses_task_col) = match task_col {
574 Some(t) => Self::sibling_column_usage(ctx, item_line, range_end, marker_col, content_col, t),
575 None => (false, false),
576 };
577
578 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
579 let actual = line.actual;
580 if actual > required
581 && !line.info.in_code_block
582 && Some(actual) != task_col
583 && !Self::starts_with_list_marker(line.trimmed)
584 && flagged_lines.insert(line.line_num)
585 {
586 let fix_target =
587 Self::compute_fix_target(actual, required, task_col, uses_content_col, uses_task_col);
588 let message = match task_col {
589 Some(t) => format!(
590 "Continuation line over-indented \
591 (expected {required} or {t}, found {actual})"
592 ),
593 None => {
594 format!("Continuation line over-indented (expected {required}, found {actual})")
595 }
596 };
597 warnings.push(Self::build_over_indent_warning(ctx, line, fix_target, message));
598 }
599 ControlFlow::Continue(())
600 });
601 }
602
603 warnings.sort_by_key(|w| (w.line, w.column));
606
607 Ok(warnings)
608 }
609
610 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
611 let warnings = self.check(ctx)?;
612 let warnings =
613 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
614 if warnings.is_empty() {
615 return Ok(ctx.content.to_string());
616 }
617
618 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
620 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
621
622 let mut content = ctx.content.to_string();
623 for fix in fixes {
624 if fix.range.start <= content.len() && fix.range.end <= content.len() {
625 content.replace_range(fix.range, &fix.replacement);
626 }
627 }
628
629 Ok(content)
630 }
631
632 fn category(&self) -> RuleCategory {
633 RuleCategory::List
634 }
635
636 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
637 ctx.content.is_empty() || ctx.list_blocks.is_empty()
638 }
639
640 fn as_any(&self) -> &dyn std::any::Any {
641 self
642 }
643
644 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
645 where
646 Self: Sized,
647 {
648 Box::new(Self)
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655 use crate::config::MarkdownFlavor;
656
657 fn check(content: &str) -> Vec<LintWarning> {
658 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
659 let rule = MD077ListContinuationIndent;
660 rule.check(&ctx).unwrap()
661 }
662
663 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
664 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
665 let rule = MD077ListContinuationIndent;
666 rule.check(&ctx).unwrap()
667 }
668
669 fn fix(content: &str) -> String {
670 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
671 let rule = MD077ListContinuationIndent;
672 rule.fix(&ctx).unwrap()
673 }
674
675 fn fix_mkdocs(content: &str) -> String {
676 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
677 let rule = MD077ListContinuationIndent;
678 rule.fix(&ctx).unwrap()
679 }
680
681 #[test]
684 fn tight_lazy_continuation_zero_indent_not_flagged() {
685 let content = "- Item\ncontinuation\n";
687 assert!(check(content).is_empty());
688 }
689
690 #[test]
691 fn tight_continuation_correct_indent_not_flagged() {
692 let content = "1. Item\n continuation\n";
694 assert!(check(content).is_empty());
695 }
696
697 #[test]
698 fn tight_continuation_over_indented_ordered() {
699 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
701 let warnings = check(content);
702 assert_eq!(warnings.len(), 1);
703 assert_eq!(warnings[0].line, 2);
704 assert!(warnings[0].message.contains("over-indented"));
705 }
706
707 #[test]
708 fn tight_continuation_over_indented_unordered() {
709 let content = "- Item\n over-indented\n";
711 let warnings = check(content);
712 assert_eq!(warnings.len(), 1);
713 assert_eq!(warnings[0].line, 2);
714 }
715
716 #[test]
717 fn tight_continuation_multiple_over_indented_lines() {
718 let content = "1. Item\n line one\n line two\n line three\n";
719 let warnings = check(content);
720 assert_eq!(warnings.len(), 3);
721 }
722
723 #[test]
724 fn tight_continuation_mixed_correct_and_over() {
725 let content = "1. Item\n correct\n over-indented\n correct again\n";
726 let warnings = check(content);
727 assert_eq!(warnings.len(), 1);
728 assert_eq!(warnings[0].line, 3);
729 }
730
731 #[test]
732 fn tight_continuation_nested_over_indented() {
733 let content = "- L1\n - L2\n over-indented continuation of L2\n";
735 let warnings = check(content);
736 assert_eq!(warnings.len(), 1);
737 assert_eq!(warnings[0].line, 3);
738 assert!(warnings[0].message.contains("expected 4"));
740 assert!(warnings[0].message.contains("found 5"));
741 }
742
743 #[test]
744 fn tight_continuation_nested_correct_indent_not_flagged() {
745 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
748 assert!(check(content).is_empty());
749 }
750
751 #[test]
752 fn fix_tight_continuation_nested_over_indented() {
753 let content = "- L1\n - L2\n over-indented continuation of L2\n";
755 let fixed = fix(content);
756 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
757 }
758
759 #[test]
760 fn tight_continuation_under_indented_not_flagged() {
761 let content = "1. Item\n under-indented\n";
764 assert!(check(content).is_empty());
765 }
766
767 #[test]
768 fn tight_continuation_tab_over_indented() {
769 let content = "- Item\n\tover-indented\n";
771 let warnings = check(content);
772 assert_eq!(warnings.len(), 1);
773 }
774
775 #[test]
776 fn fix_tight_continuation_over_indented_ordered() {
777 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
778 let fixed = fix(content);
779 assert_eq!(
780 fixed,
781 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
782 );
783 }
784
785 #[test]
786 fn fix_tight_continuation_over_indented_unordered() {
787 let content = "- Item\n over-indented\n";
788 let fixed = fix(content);
789 assert_eq!(fixed, "- Item\n over-indented\n");
790 }
791
792 #[test]
793 fn fix_tight_continuation_multiple_lines() {
794 let content = "1. Item\n line one\n line two\n";
795 let fixed = fix(content);
796 assert_eq!(fixed, "1. Item\n line one\n line two\n");
797 }
798
799 #[test]
800 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
801 let content = "1. Item\n continuation\n";
804 assert!(check_mkdocs(content).is_empty());
805 }
806
807 #[test]
808 fn tight_continuation_mkdocs_5space_ordered_flagged() {
809 let content = "1. Item\n over-indented\n";
811 let warnings = check_mkdocs(content);
812 assert_eq!(warnings.len(), 1);
813 assert!(warnings[0].message.contains("expected 4"));
814 assert!(warnings[0].message.contains("found 5"));
815 }
816
817 #[test]
818 fn fix_tight_continuation_mkdocs_over_indented() {
819 let content = "1. Item\n over-indented\n";
820 let fixed = fix_mkdocs(content);
821 assert_eq!(fixed, "1. Item\n over-indented\n");
822 }
823
824 #[test]
825 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
826 let content = "* Level 0\n * Level 1\n * Level 2\n";
829 assert!(check(content).is_empty());
830 }
831
832 #[test]
833 fn tight_continuation_ordered_marker_not_flagged() {
834 let content = "- Parent\n 1. Child item\n";
836 assert!(check(content).is_empty());
837 }
838
839 #[test]
842 fn unordered_correct_indent_no_warning() {
843 let content = "- Item\n\n continuation\n";
844 assert!(check(content).is_empty());
845 }
846
847 #[test]
848 fn unordered_partial_indent_warns() {
849 let content = "- Item\n\n continuation\n";
852 let warnings = check(content);
853 assert_eq!(warnings.len(), 1);
854 assert_eq!(warnings[0].line, 3);
855 assert!(warnings[0].message.contains("2 spaces"));
856 assert!(warnings[0].message.contains("found 1"));
857 }
858
859 #[test]
860 fn unordered_zero_indent_is_new_paragraph() {
861 let content = "- Item\n\ncontinuation\n";
864 assert!(check(content).is_empty());
865 }
866
867 #[test]
870 fn ordered_3space_correct_commonmark() {
871 let content = "1. Item\n\n continuation\n";
873 assert!(check(content).is_empty());
874 }
875
876 #[test]
877 fn ordered_2space_under_indent_commonmark() {
878 let content = "1. Item\n\n continuation\n";
879 let warnings = check(content);
880 assert_eq!(warnings.len(), 1);
881 assert!(warnings[0].message.contains("3 spaces"));
882 assert!(warnings[0].message.contains("found 2"));
883 }
884
885 #[test]
888 fn multi_digit_marker_correct() {
889 let content = "10. Item\n\n continuation\n";
891 assert!(check(content).is_empty());
892 }
893
894 #[test]
895 fn multi_digit_marker_under_indent() {
896 let content = "10. Item\n\n continuation\n";
897 let warnings = check(content);
898 assert_eq!(warnings.len(), 1);
899 assert!(warnings[0].message.contains("4 spaces"));
900 }
901
902 #[test]
905 fn mkdocs_3space_ordered_warns() {
906 let content = "1. Item\n\n continuation\n";
908 let warnings = check_mkdocs(content);
909 assert_eq!(warnings.len(), 1);
910 assert!(warnings[0].message.contains("4 spaces"));
911 assert!(warnings[0].message.contains("MkDocs"));
912 }
913
914 #[test]
915 fn mkdocs_4space_ordered_no_warning() {
916 let content = "1. Item\n\n continuation\n";
917 assert!(check_mkdocs(content).is_empty());
918 }
919
920 #[test]
921 fn mkdocs_unordered_2space_ok() {
922 let content = "- Item\n\n continuation\n";
924 assert!(check_mkdocs(content).is_empty());
925 }
926
927 #[test]
928 fn mkdocs_unordered_2space_warns() {
929 let content = "- Item\n\n continuation\n";
931 let warnings = check_mkdocs(content);
932 assert_eq!(warnings.len(), 1);
933 assert!(warnings[0].message.contains("4 spaces"));
934 }
935
936 #[test]
939 fn fix_unordered_indent() {
940 let content = "- Item\n\n continuation\n";
942 let fixed = fix(content);
943 assert_eq!(fixed, "- Item\n\n continuation\n");
944 }
945
946 #[test]
947 fn fix_ordered_indent() {
948 let content = "1. Item\n\n continuation\n";
949 let fixed = fix(content);
950 assert_eq!(fixed, "1. Item\n\n continuation\n");
951 }
952
953 #[test]
954 fn fix_mkdocs_indent() {
955 let content = "1. Item\n\n continuation\n";
956 let fixed = fix_mkdocs(content);
957 assert_eq!(fixed, "1. Item\n\n continuation\n");
958 }
959
960 #[test]
963 fn nested_list_items_not_flagged() {
964 let content = "- Parent\n\n - Child\n";
965 assert!(check(content).is_empty());
966 }
967
968 #[test]
969 fn nested_list_zero_indent_is_new_paragraph() {
970 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
972 assert!(check(content).is_empty());
973 }
974
975 #[test]
976 fn nested_list_partial_indent_flagged() {
977 let content = "- Parent\n - Child\n\n continuation of parent\n";
979 let warnings = check(content);
980 assert_eq!(warnings.len(), 1);
981 assert!(warnings[0].message.contains("2 spaces"));
982 }
983
984 #[test]
987 fn code_block_correctly_indented_no_warning() {
988 let content = "- Item\n\n ```\n code\n ```\n";
990 assert!(check(content).is_empty());
991 }
992
993 #[test]
994 fn code_fence_under_indented_warns() {
995 let content = "- Item\n\n ```\n code\n ```\n";
999 let warnings = check(content);
1000 assert_eq!(warnings.len(), 1);
1001 assert_eq!(warnings[0].line, 3);
1002 }
1003
1004 #[test]
1005 fn code_fence_under_indented_ordered_mkdocs() {
1006 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
1009 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
1011 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1013 assert!(warnings[0].message.contains("4 spaces"));
1014 assert!(warnings[0].message.contains("MkDocs"));
1015 }
1016
1017 #[test]
1018 fn code_fence_tilde_under_indented() {
1019 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1020 let warnings = check(content);
1021 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1023 }
1024
1025 #[test]
1028 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
1029 let content = "- Item\n\n\ncontinuation\n";
1031 assert!(check(content).is_empty());
1032 }
1033
1034 #[test]
1035 fn multiple_blank_lines_partial_indent_flags() {
1036 let content = "- Item\n\n\n continuation\n";
1037 let warnings = check(content);
1038 assert_eq!(warnings.len(), 1);
1039 }
1040
1041 #[test]
1044 fn empty_item_no_warning() {
1045 let content = "- \n- Second\n";
1046 assert!(check(content).is_empty());
1047 }
1048
1049 #[test]
1052 fn multiple_items_mixed_indent() {
1053 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
1054 let warnings = check(content);
1055 assert_eq!(warnings.len(), 1);
1056 assert_eq!(warnings[0].line, 7);
1057 }
1058
1059 #[test]
1062 fn task_list_correct_indent() {
1063 let content = "- [ ] Task\n\n continuation\n";
1065 assert!(check(content).is_empty());
1066 }
1067
1068 #[test]
1071 fn frontmatter_not_flagged() {
1072 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
1073 assert!(check(content).is_empty());
1074 }
1075
1076 #[test]
1079 fn fix_multiple_items() {
1080 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
1081 let fixed = fix(content);
1082 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
1083 }
1084
1085 #[test]
1086 fn fix_multiline_loose_continuation_all_lines() {
1087 let content = "1. Item\n\n line one\n line two\n line three\n";
1088 let fixed = fix(content);
1089 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
1090 }
1091
1092 #[test]
1095 fn sibling_item_boundary_respected() {
1096 let content = "- First\n- Second\n\n continuation\n";
1098 assert!(check(content).is_empty());
1099 }
1100
1101 #[test]
1104 fn blockquote_list_correct_indent_no_warning() {
1105 let content = "> - Item\n>\n> continuation\n";
1108 assert!(check(content).is_empty());
1109 }
1110
1111 #[test]
1112 fn blockquote_list_under_indent_no_false_positive() {
1113 let content = "> - Item\n>\n> continuation\n";
1118 assert!(check(content).is_empty());
1119 }
1120
1121 #[test]
1124 fn deeply_nested_correct_indent() {
1125 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1126 assert!(check(content).is_empty());
1127 }
1128
1129 #[test]
1130 fn deeply_nested_under_indent() {
1131 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1134 let warnings = check(content);
1135 assert_eq!(warnings.len(), 1);
1136 assert!(warnings[0].message.contains("6 spaces"));
1137 assert!(warnings[0].message.contains("found 5"));
1138 }
1139
1140 #[test]
1143 fn loose_tab_continuation_over_indented() {
1144 let content = "- Item\n\n\tcontinuation\n";
1149 let warnings = check(content);
1150 assert_eq!(warnings.len(), 1);
1151 assert_eq!(warnings[0].line, 3);
1152 assert_eq!(fix(content), "- Item\n\n continuation\n");
1153 }
1154
1155 #[test]
1158 fn multiple_continuations_correct() {
1159 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
1160 assert!(check(content).is_empty());
1161 }
1162
1163 #[test]
1164 fn multiple_continuations_second_under_indent() {
1165 let content = "- Item\n\n para 1\n\n continuation 2\n";
1167 let warnings = check(content);
1168 assert_eq!(warnings.len(), 1);
1169 assert_eq!(warnings[0].line, 5);
1170 }
1171
1172 #[test]
1175 fn ordered_paren_marker_correct() {
1176 let content = "1) Item\n\n continuation\n";
1178 assert!(check(content).is_empty());
1179 }
1180
1181 #[test]
1182 fn ordered_paren_marker_under_indent() {
1183 let content = "1) Item\n\n continuation\n";
1184 let warnings = check(content);
1185 assert_eq!(warnings.len(), 1);
1186 assert!(warnings[0].message.contains("3 spaces"));
1187 }
1188
1189 #[test]
1192 fn star_marker_correct() {
1193 let content = "* Item\n\n continuation\n";
1194 assert!(check(content).is_empty());
1195 }
1196
1197 #[test]
1198 fn star_marker_under_indent() {
1199 let content = "* Item\n\n continuation\n";
1200 let warnings = check(content);
1201 assert_eq!(warnings.len(), 1);
1202 }
1203
1204 #[test]
1205 fn plus_marker_correct() {
1206 let content = "+ Item\n\n continuation\n";
1207 assert!(check(content).is_empty());
1208 }
1209
1210 #[test]
1213 fn heading_after_list_no_warning() {
1214 let content = "- Item\n\n# Heading\n";
1215 assert!(check(content).is_empty());
1216 }
1217
1218 #[test]
1221 fn hr_after_list_no_warning() {
1222 let content = "- Item\n\n---\n";
1223 assert!(check(content).is_empty());
1224 }
1225
1226 #[test]
1229 fn reference_link_def_not_flagged() {
1230 let content = "- Item\n\n [link]: https://example.com\n";
1231 assert!(check(content).is_empty());
1232 }
1233
1234 #[test]
1237 fn footnote_def_not_flagged() {
1238 let content = "- Item\n\n [^1]: footnote text\n";
1239 assert!(check(content).is_empty());
1240 }
1241
1242 #[test]
1243 fn footnote_multiline_body_after_list_not_flagged() {
1244 let content = "# A list followed by a footnote\n\n\
1248 Here is a paragraph.[^fn]\n\n\
1249 - This is a list.\n\n\
1250 [^fn]:\n\
1251 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1252 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1253 assert!(check(content).is_empty());
1254 }
1255
1256 #[test]
1257 fn fix_footnote_multiline_body_after_list_is_noop() {
1258 let content = "# A list followed by a footnote\n\n\
1262 Here is a paragraph.[^fn]\n\n\
1263 - This is a list.\n\n\
1264 [^fn]:\n\
1265 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1266 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1267 assert_eq!(fix(content), content);
1268 }
1269
1270 #[test]
1271 fn footnote_body_indented_past_list_content_col_not_flagged() {
1272 let content = "- Item\n\n[^fn]:\n Body line one.\n Body line two.\n";
1276 assert!(check(content).is_empty());
1277 }
1278
1279 #[test]
1280 fn list_inside_footnote_body_continuation_not_flagged() {
1281 let content = "Text.[^fn]\n\n[^fn]:\n\
1285 \x20\x20\x20\x20- nested item\n\
1286 \x20\x20\x20\x20\x20\x20\x20over-indented continuation\n";
1287 assert!(check(content).is_empty());
1288 }
1289
1290 #[test]
1291 fn footnote_multiline_body_after_list_not_flagged_mkdocs() {
1292 let content = "Here is a paragraph.[^fn]\n\n\
1296 - This is a list.\n\n\
1297 [^fn]:\n\
1298 \x20\x20\x20\x20\x20\x20Footnote body that spans\n\
1299 \x20\x20\x20\x20\x20\x20multiple indented lines.\n";
1300 assert!(check_mkdocs(content).is_empty());
1301 }
1302
1303 #[test]
1306 fn fix_deeply_nested() {
1307 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
1308 let fixed = fix(content);
1309 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
1310 }
1311
1312 #[test]
1313 fn fix_mkdocs_unordered() {
1314 let content = "- Item\n\n continuation\n";
1316 let fixed = fix_mkdocs(content);
1317 assert_eq!(fixed, "- Item\n\n continuation\n");
1318 }
1319
1320 #[test]
1321 fn fix_code_fence_indent() {
1322 let content = "- Item\n\n ```\n code\n ```\n";
1325 let fixed = fix(content);
1326 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
1327 }
1328
1329 #[test]
1330 fn fix_mkdocs_code_fence_indent() {
1331 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
1333 let fixed = fix_mkdocs(content);
1334 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
1335 }
1336
1337 #[test]
1340 fn empty_document_no_warning() {
1341 assert!(check("").is_empty());
1342 }
1343
1344 #[test]
1345 fn whitespace_only_no_warning() {
1346 assert!(check(" \n\n \n").is_empty());
1347 }
1348
1349 #[test]
1352 fn no_list_no_warning() {
1353 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
1354 assert!(check(content).is_empty());
1355 }
1356
1357 #[test]
1360 fn multiline_continuation_all_lines_flagged() {
1361 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";
1362 let warnings = check(content);
1363 assert_eq!(warnings.len(), 3);
1364 assert_eq!(warnings[0].line, 3);
1365 assert_eq!(warnings[1].line, 4);
1366 assert_eq!(warnings[2].line, 5);
1367 }
1368
1369 #[test]
1370 fn multiline_continuation_with_frontmatter_fix() {
1371 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";
1372 let fixed = fix(content);
1373 assert_eq!(
1374 fixed,
1375 "---\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"
1376 );
1377 }
1378
1379 #[test]
1380 fn multiline_continuation_correct_indent_no_warning() {
1381 let content = "1. Item\n\n line one\n line two\n line three\n";
1382 assert!(check(content).is_empty());
1383 }
1384
1385 #[test]
1386 fn multiline_continuation_mixed_indent() {
1387 let content = "1. Item\n\n correct\n wrong\n correct\n";
1388 let warnings = check(content);
1389 assert_eq!(warnings.len(), 1);
1390 assert_eq!(warnings[0].line, 4);
1391 }
1392
1393 #[test]
1394 fn multiline_continuation_unordered() {
1395 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
1396 let warnings = check(content);
1397 assert_eq!(warnings.len(), 3);
1398 let fixed = fix(content);
1399 assert_eq!(
1400 fixed,
1401 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
1402 );
1403 }
1404
1405 #[test]
1406 fn multiline_continuation_two_items_fix() {
1407 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
1408 let fixed = fix(content);
1409 assert_eq!(
1410 fixed,
1411 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1412 );
1413 }
1414
1415 #[test]
1416 fn fence_fix_does_not_break_pairing_for_md031() {
1417 let content = "#### title\n\nabc\n\n\
1424 1. ab\n\n\
1425 \x20\x20`aabbccdd`\n\n\
1426 2. cd\n\n\
1427 \x20\x20`bbcc dd ee`\n\n\
1428 \x20\x20```\n\
1429 \x20\x20abcd\n\
1430 \x20\x20ef gh\n\
1431 \x20\x20```\n\n\
1432 \x20\x20uu\n\n\
1433 \x20\x20```\n\
1434 \x20\x20cdef\n\
1435 \x20\x20gh ij\n\
1436 \x20\x20```\n";
1437 let expected = "#### title\n\nabc\n\n\
1438 1. ab\n\n\
1439 \x20\x20\x20`aabbccdd`\n\n\
1440 2. cd\n\n\
1441 \x20\x20\x20`bbcc dd ee`\n\n\
1442 \x20\x20\x20```\n\
1443 \x20\x20\x20abcd\n\
1444 \x20\x20\x20ef gh\n\
1445 \x20\x20\x20```\n\n\
1446 \x20\x20\x20uu\n\n\
1447 \x20\x20\x20```\n\
1448 \x20\x20\x20cdef\n\
1449 \x20\x20\x20gh ij\n\
1450 \x20\x20\x20```\n";
1451 assert_eq!(fix(content), expected);
1452 }
1453
1454 #[test]
1455 fn multiline_continuation_separated_by_blank() {
1456 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1457 let warnings = check(content);
1458 assert_eq!(warnings.len(), 4);
1459 let fixed = fix(content);
1460 assert_eq!(
1461 fixed,
1462 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1463 );
1464 }
1465
1466 #[test]
1467 fn tab_indented_fence_is_normalized_to_spaces() {
1468 let content = "100. ab\n\n\t```\n\tabcd\n\t```\n";
1476 let expected = "100. ab\n\n ```\n abcd\n ```\n";
1477 assert_eq!(fix(content), expected);
1478 }
1479
1480 #[test]
1489 fn loose_continuation_over_indented_flagged() {
1490 let content = "* Item\n\n over-indented\n";
1493 let warnings = check(content);
1494 assert_eq!(warnings.len(), 1);
1495 assert_eq!(warnings[0].line, 3);
1496 assert!(warnings[0].message.contains("over-indented"));
1497 assert!(warnings[0].message.contains("expected 2"));
1498 assert!(warnings[0].message.contains("found 3"));
1499 }
1500
1501 #[test]
1502 fn loose_continuation_over_indented_multiline_mixed() {
1503 let content = "* Item\n\n over one\n correct\n over two\n";
1505 let warnings = check(content);
1506 assert_eq!(warnings.len(), 2);
1507 assert_eq!(warnings[0].line, 3);
1508 assert_eq!(warnings[1].line, 5);
1509 }
1510
1511 #[test]
1512 fn fix_loose_continuation_over_indented() {
1513 let content = "* Item\n\n over one\n correct\n over two\n";
1514 let fixed = fix(content);
1515 assert_eq!(fixed, "* Item\n\n over one\n correct\n over two\n");
1516 }
1517
1518 #[test]
1519 fn fix_tight_and_loose_items_normalized_identically() {
1520 let content = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1523 * 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\
1524 * 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";
1525 let expected = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1526 * 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\
1527 * 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";
1528 assert_eq!(fix(content), expected);
1529 }
1530
1531 #[test]
1532 fn multi_paragraph_item_loose_paragraph_over_indented() {
1533 let content = "* Item.\n tight over\n\n loose over\n";
1536 let warnings = check(content);
1537 assert_eq!(warnings.len(), 2);
1538 assert_eq!(warnings[0].line, 2);
1539 assert_eq!(warnings[1].line, 4);
1540 }
1541
1542 #[test]
1543 fn loose_indented_code_block_not_flagged() {
1544 let content = "- Item\n\n code line\n";
1548 assert!(check(content).is_empty());
1549 }
1550
1551 #[test]
1552 fn mkdocs_loose_over_indented_flagged() {
1553 let content = "1. Item\n\n over\n";
1556 let warnings = check_mkdocs(content);
1557 assert_eq!(warnings.len(), 1);
1558 assert_eq!(warnings[0].line, 3);
1559 assert!(warnings[0].message.contains("over-indented"));
1560 assert!(warnings[0].message.contains("expected 4"));
1561 assert!(warnings[0].message.contains("found 5"));
1562 }
1563
1564 #[test]
1565 fn task_list_loose_over_indented_flagged() {
1566 let content = "- [ ] Task\n\n over\n";
1569 let warnings = check(content);
1570 assert_eq!(warnings.len(), 1);
1571 assert_eq!(warnings[0].line, 3);
1572 }
1573
1574 #[test]
1575 fn loose_over_indent_boundary_below_code_block_threshold_flagged() {
1576 let content = "- Item\n\n over\n";
1581 let warnings = check(content);
1582 assert_eq!(warnings.len(), 1);
1583 assert_eq!(warnings[0].line, 3);
1584 assert!(warnings[0].message.contains("expected 2"));
1585 assert!(warnings[0].message.contains("found 5"));
1586 }
1587
1588 #[test]
1589 fn loose_over_indent_does_not_steal_nested_under_indent() {
1590 let content = "- Outer\n - Inner\n\n continuation\n";
1597 let warnings = check(content);
1598 assert_eq!(warnings.len(), 1);
1599 assert_eq!(warnings[0].line, 4);
1600 assert!(warnings[0].message.contains("4 spaces"));
1601 assert!(warnings[0].message.contains("found 3"));
1602 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1603 }
1604
1605 #[test]
1606 fn loose_over_indent_attributes_to_deepest_enclosing_item() {
1607 let content = "- Outer\n - Inner\n\n continuation\n";
1611 let warnings = check(content);
1612 assert_eq!(warnings.len(), 1);
1613 assert_eq!(warnings[0].line, 4);
1614 assert!(warnings[0].message.contains("expected 4"));
1615 assert!(warnings[0].message.contains("found 5"));
1616 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1617 }
1618
1619 #[test]
1628 fn loose_over_indented_fence_not_flagged() {
1629 let content = "- Item\n\n ```\n code\n ```\n";
1630 assert!(check(content).is_empty());
1631 assert_eq!(fix(content), content);
1632 }
1633
1634 #[test]
1635 fn tight_over_indented_fence_not_flagged() {
1636 let content = "- Item\n ```\n code\n ```\n";
1637 assert!(check(content).is_empty());
1638 assert_eq!(fix(content), content);
1639 }
1640
1641 #[test]
1642 fn over_indented_tilde_fence_not_flagged() {
1643 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1644 assert!(check(content).is_empty());
1645 assert_eq!(fix(content), content);
1646 }
1647
1648 #[test]
1649 fn fence_like_code_content_inside_fenced_block_not_flagged() {
1650 let content = "- Item\n\n ~~~\n ```\n ~~~\n";
1653 assert!(check(content).is_empty());
1654 assert_eq!(fix(content), content);
1655 }
1656
1657 #[test]
1658 fn unterminated_over_indented_fence_not_flagged() {
1659 let content = "- Item\n\n ```\n code1\n code2deeper\n";
1662 assert!(check(content).is_empty());
1663 assert_eq!(fix(content), content);
1664 }
1665
1666 #[test]
1674 fn task_list_tight_continuation_post_checkbox_reproducer_579() {
1675 let content = "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n";
1678 assert!(check(content).is_empty());
1679 }
1680
1681 #[test]
1682 fn task_list_tight_continuation_dash_unchecked() {
1683 let content = "- [ ] Task\n continuation\n";
1684 assert!(check(content).is_empty());
1685 }
1686
1687 #[test]
1688 fn task_list_tight_continuation_dash_checked_lower() {
1689 let content = "- [x] Task\n continuation\n";
1690 assert!(check(content).is_empty());
1691 }
1692
1693 #[test]
1694 fn task_list_tight_continuation_dash_checked_upper() {
1695 let content = "- [X] Task\n continuation\n";
1696 assert!(check(content).is_empty());
1697 }
1698
1699 #[test]
1700 fn task_list_tight_continuation_star_marker() {
1701 let content = "* [ ] Task\n continuation\n";
1702 assert!(check(content).is_empty());
1703 }
1704
1705 #[test]
1706 fn task_list_tight_continuation_plus_marker() {
1707 let content = "+ [ ] Task\n continuation\n";
1708 assert!(check(content).is_empty());
1709 }
1710
1711 #[test]
1712 fn task_list_tight_continuation_content_column_still_valid() {
1713 let content = "- [ ] Task\n continuation\n";
1716 assert!(check(content).is_empty());
1717 }
1718
1719 #[test]
1720 fn task_list_tight_continuation_between_columns_still_flagged() {
1721 let content = "- [ ] Task\n continuation\n";
1724 let warnings = check(content);
1725 assert_eq!(warnings.len(), 1);
1726 assert!(warnings[0].message.contains("expected 2 or 6"));
1728 assert!(warnings[0].message.contains("found 4"));
1729 }
1730
1731 #[test]
1732 fn task_list_tight_continuation_overshoot_still_flagged() {
1733 let content = "- [ ] Task\n continuation\n";
1735 let warnings = check(content);
1736 assert_eq!(warnings.len(), 1);
1737 assert!(warnings[0].message.contains("expected 2 or 6"));
1738 assert!(warnings[0].message.contains("found 7"));
1739 }
1740
1741 #[test]
1744 fn fix_task_list_overshoot_snaps_to_task_col() {
1745 let content = "- [ ] Task\n continuation\n";
1749 let fixed = fix(content);
1750 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1751 }
1752
1753 #[test]
1754 fn fix_task_list_col_5_snaps_to_task_col() {
1755 let content = "- [ ] Task\n continuation\n";
1757 let fixed = fix(content);
1758 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1759 }
1760
1761 #[test]
1762 fn fix_task_list_col_3_snaps_to_content_col() {
1763 let content = "- [ ] Task\n continuation\n";
1765 let fixed = fix(content);
1766 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1767 }
1768
1769 #[test]
1770 fn fix_task_list_col_4_ties_to_content_col() {
1771 let content = "- [ ] Task\n continuation\n";
1776 let fixed = fix(content);
1777 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1778 }
1779
1780 #[test]
1781 fn fix_task_list_ordered_overshoot_snaps_to_task_col() {
1782 let content = "1. [ ] Task\n continuation\n";
1785 let fixed = fix(content);
1786 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1787 }
1788
1789 #[test]
1790 fn fix_task_list_ordered_under_overshoot_snaps_to_content_col() {
1791 let content = "1. [ ] Task\n continuation\n";
1794 let fixed = fix(content);
1795 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1796 }
1797
1798 #[test]
1799 fn task_list_tight_continuation_ordered_single_digit() {
1800 let content = "1. [ ] Task\n continuation\n";
1802 assert!(check(content).is_empty());
1803 }
1804
1805 #[test]
1806 fn task_list_tight_continuation_ordered_multi_digit() {
1807 let content = "10. [ ] Task\n continuation\n";
1809 assert!(check(content).is_empty());
1810 }
1811
1812 #[test]
1813 fn task_list_tight_continuation_nested_dash() {
1814 let content = "- Parent\n - [ ] Nested task\n continuation\n";
1816 assert!(check(content).is_empty());
1817 }
1818
1819 #[test]
1820 fn task_list_loose_continuation_post_checkbox_column_not_flagged() {
1821 let content = "- [ ] Task\n\n continuation\n";
1826 assert!(check(content).is_empty());
1827 }
1828
1829 #[test]
1830 fn task_list_empty_body_is_not_a_task() {
1831 let content = "- [ ]\n continuation\n";
1837 let warnings = check(content);
1838 assert_eq!(warnings.len(), 1);
1839 assert!(warnings[0].message.contains("found 4"));
1840 }
1841
1842 #[test]
1843 fn task_list_malformed_checkbox_is_not_a_task() {
1844 let content = "- [~] Not a task\n continuation\n";
1846 let warnings = check(content);
1847 assert_eq!(warnings.len(), 1);
1848 }
1849
1850 #[test]
1857 fn task_list_mkdocs_unordered_required_min_valid() {
1858 let content = "- [ ] Task\n continuation\n";
1860 assert!(check_mkdocs(content).is_empty());
1861 }
1862
1863 #[test]
1864 fn task_list_mkdocs_unordered_post_checkbox_valid() {
1865 let content = "- [ ] Task\n continuation\n";
1866 assert!(check_mkdocs(content).is_empty());
1867 }
1868
1869 #[test]
1870 fn task_list_mkdocs_unordered_between_flagged() {
1871 let content = "- [ ] Task\n continuation\n";
1873 let warnings = check_mkdocs(content);
1874 assert_eq!(warnings.len(), 1);
1875 }
1876
1877 #[test]
1878 fn task_list_mkdocs_ordered_both_columns_valid() {
1879 let at_4 = "1. [ ] Task\n continuation\n";
1881 assert!(check_mkdocs(at_4).is_empty());
1882 let at_7 = "1. [ ] Task\n continuation\n";
1883 assert!(check_mkdocs(at_7).is_empty());
1884 }
1885
1886 #[test]
1887 fn task_list_mkdocs_ordered_between_flagged() {
1888 let at_5 = "1. [ ] Task\n continuation\n";
1890 assert_eq!(check_mkdocs(at_5).len(), 1);
1891 let at_6 = "1. [ ] Task\n continuation\n";
1892 assert_eq!(check_mkdocs(at_6).len(), 1);
1893 }
1894
1895 #[test]
1905 fn fix_task_list_tie_sibling_at_task_col_snaps_to_task_col() {
1906 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
1910 let fixed = fix(content);
1911 assert_eq!(
1912 fixed,
1913 "- [ ] Task\n aligned continuation\n tied continuation\n"
1914 );
1915 }
1916
1917 #[test]
1918 fn fix_task_list_tie_sibling_at_content_col_snaps_to_content_col() {
1919 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
1922 let fixed = fix(content);
1923 assert_eq!(fixed, "- [ ] Task\n aligned continuation\n tied continuation\n");
1924 }
1925
1926 #[test]
1927 fn fix_task_list_tie_both_siblings_snaps_to_content_col() {
1928 let content = "- [ ] Task\n at content col\n at task col\n tied continuation\n";
1932 let fixed = fix(content);
1933 assert_eq!(
1934 fixed,
1935 "- [ ] Task\n at content col\n at task col\n tied continuation\n"
1936 );
1937 }
1938
1939 #[test]
1940 fn fix_task_list_tie_sees_task_col_through_tight_lazy_continuation() {
1941 let content = concat!("- [ ] Task\n", "lazy\n", " aligned at task col\n", " tied\n",);
1955 let fixed = fix(content);
1956 assert!(
1957 fixed.contains("\n tied\n"),
1958 "tied line should snap to col 6 (task col) because a task-col \
1959 sibling is visible past the tight lazy-continuation line; got:\n{fixed}"
1960 );
1961 }
1962
1963 #[test]
1970 fn task_list_tab_indented_continuation_flagged() {
1971 let content = "- [ ] Task\n\t\twrap\n";
1974 let warnings = check(content);
1975 assert_eq!(warnings.len(), 1);
1976 assert!(warnings[0].message.contains("expected 2 or 6"));
1977 assert!(warnings[0].message.contains("found 8"));
1978 }
1979
1980 #[test]
1981 fn fix_task_list_tab_indented_snaps_to_task_col() {
1982 let content = "- [ ] Task\n\t\twrap\n";
1984 let fixed = fix(content);
1985 assert_eq!(fixed, "- [ ] Task\n wrap\n");
1986 }
1987
1988 #[test]
1989 fn fix_task_list_single_tab_equidistant_snaps_to_content_col() {
1990 let content = "- [ ] Task\n\twrap\n";
1993 let fixed = fix(content);
1994 assert_eq!(fixed, "- [ ] Task\n wrap\n");
1995 }
1996
1997 #[test]
2007 fn task_list_blockquote_post_checkbox_not_flagged() {
2008 let content = "> - [ ] Task\n> continuation\n";
2010 assert!(check(content).is_empty());
2011 }
2012
2013 #[test]
2014 fn task_list_blockquote_between_cols_documented_limitation() {
2015 let content = "> - [ ] Task\n> continuation\n";
2019 assert!(check(content).is_empty());
2020 }
2021
2022 #[test]
2023 fn task_list_blockquote_overshoot_documented_limitation() {
2024 let content = "> - [ ] Task\n> continuation\n";
2026 assert!(check(content).is_empty());
2027 }
2028
2029 #[test]
2036 fn fix_task_list_mkdocs_unordered_overshoot_snaps_to_task_col() {
2037 let content = "- [ ] Task\n continuation\n";
2040 let fixed = fix_mkdocs(content);
2041 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2042 }
2043
2044 #[test]
2045 fn fix_task_list_mkdocs_unordered_tie_snaps_to_required() {
2046 let content = "- [ ] Task\n continuation\n";
2049 let fixed = fix_mkdocs(content);
2050 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2051 }
2052
2053 #[test]
2054 fn fix_task_list_mkdocs_ordered_overshoot_snaps_to_task_col() {
2055 let content = "1. [ ] Task\n continuation\n";
2058 let fixed = fix_mkdocs(content);
2059 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2060 }
2061
2062 #[test]
2063 fn fix_task_list_mkdocs_ordered_near_required_snaps_to_required() {
2064 let content = "1. [ ] Task\n continuation\n";
2070 let fixed = fix_mkdocs(content);
2071 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2072 }
2073
2074 #[test]
2075 fn fix_task_list_mkdocs_ordered_between_cols_snaps_to_task_col() {
2076 let content = "1. [ ] Task\n continuation\n";
2079 let fixed = fix_mkdocs(content);
2080 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2081 }
2082
2083 fn assert_idempotent(content: &str) {
2093 let once = fix(content);
2094 let twice = fix(&once);
2095 assert_eq!(once, twice, "MD077 fix was not idempotent on input: {content:?}");
2096 }
2097
2098 fn assert_idempotent_mkdocs(content: &str) {
2099 let once = fix_mkdocs(content);
2100 let twice = fix_mkdocs(&once);
2101 assert_eq!(
2102 once, twice,
2103 "MD077 (MkDocs) fix was not idempotent on input: {content:?}"
2104 );
2105 }
2106
2107 #[test]
2108 fn idempotent_task_list_between_cols() {
2109 assert_idempotent("- [ ] Task\n continuation\n");
2110 }
2111
2112 #[test]
2113 fn idempotent_task_list_overshoot() {
2114 assert_idempotent("- [ ] Task\n continuation\n");
2115 }
2116
2117 #[test]
2118 fn idempotent_task_list_under_post_checkbox() {
2119 assert_idempotent("- [ ] Task\n continuation\n");
2120 }
2121
2122 #[test]
2123 fn idempotent_task_list_near_post_checkbox() {
2124 assert_idempotent("- [ ] Task\n continuation\n");
2125 }
2126
2127 #[test]
2128 fn idempotent_task_list_tab_overshoot() {
2129 assert_idempotent("- [ ] Task\n\t\twrap\n");
2130 }
2131
2132 #[test]
2133 fn idempotent_task_list_single_tab() {
2134 assert_idempotent("- [ ] Task\n\twrap\n");
2135 }
2136
2137 #[test]
2138 fn idempotent_task_list_ordered_overshoot() {
2139 assert_idempotent("1. [ ] Task\n continuation\n");
2140 }
2141
2142 #[test]
2143 fn idempotent_task_list_ordered_under() {
2144 assert_idempotent("1. [ ] Task\n continuation\n");
2145 }
2146
2147 #[test]
2148 fn idempotent_task_list_tie_with_sibling_at_task_col() {
2149 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2150 }
2151
2152 #[test]
2153 fn idempotent_task_list_tie_with_sibling_at_content_col() {
2154 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2155 }
2156
2157 #[test]
2158 fn idempotent_task_list_mkdocs_unordered_overshoot() {
2159 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2160 }
2161
2162 #[test]
2163 fn idempotent_task_list_mkdocs_unordered_tie() {
2164 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2165 }
2166
2167 #[test]
2168 fn idempotent_task_list_mkdocs_ordered_overshoot() {
2169 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2170 }
2171
2172 #[test]
2173 fn idempotent_task_list_mkdocs_ordered_between() {
2174 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2175 }
2176
2177 #[test]
2178 fn idempotent_task_list_reproducer_579() {
2179 assert_idempotent(
2183 "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n",
2184 );
2185 }
2186
2187 #[test]
2188 fn idempotent_non_task_list_still_holds() {
2189 assert_idempotent("1. Item\n over-indented\n");
2192 assert_idempotent("- Item\n\n continuation\n");
2193 }
2194
2195 #[test]
2202 fn idempotent_non_task_loose_under_indent_ordered() {
2203 assert_idempotent("1. Item\n\n continuation\n");
2205 }
2206
2207 #[test]
2208 fn idempotent_non_task_loose_under_indent_multi_digit() {
2209 assert_idempotent("10. Item\n\n continuation\n");
2211 }
2212
2213 #[test]
2214 fn idempotent_non_task_tight_over_indent_ordered() {
2215 assert_idempotent("1. Item\n over-indented\n");
2217 }
2218
2219 #[test]
2227 fn idempotent_non_task_fence_ordered_loose() {
2228 assert_idempotent("1. Item\n\n ```rust\n let x = 1;\n ```\n");
2230 }
2231
2232 #[test]
2233 fn idempotent_non_task_fence_tilde_under_indent() {
2234 assert_idempotent("1. Item\n\n ~~~\nplain text\n ~~~\n");
2240 }
2241
2242 #[test]
2243 fn idempotent_non_task_fence_interior_above_required() {
2244 assert_idempotent("1. Item\n\n ```\n deeply indented code\n ```\n");
2248 }
2249
2250 #[test]
2251 fn fence_fix_promotes_interior_below_scope_in_single_pass() {
2252 let content = "1. Item\n\n ```\ncode\n ```\n";
2256 let fixed = fix(content);
2257 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2258 }
2259
2260 #[test]
2261 fn fence_fix_preserves_interior_above_required() {
2262 let content = "1. Item\n\n ```\n code\n ```\n";
2265 let fixed = fix(content);
2266 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2267 }
2268
2269 #[test]
2276 fn idempotent_non_task_mkdocs_ordered_at_3_spaces() {
2277 assert_idempotent_mkdocs("1. Item\n\n continuation\n");
2279 }
2280
2281 #[test]
2282 fn idempotent_non_task_mkdocs_unordered_at_2_spaces() {
2283 assert_idempotent_mkdocs("- Item\n\n continuation\n");
2285 }
2286
2287 #[test]
2288 fn idempotent_non_task_mkdocs_fence_compound() {
2289 assert_idempotent_mkdocs("1. Item\n\n ```toml\n k = 1\n ```\n");
2291 }
2292}