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)]
22pub struct MD077ListContinuationIndent;
23
24impl MD077ListContinuationIndent {
25 const TASK_CHECKBOX_PREFIX_LEN: usize = 4;
28
29 fn is_task_list_item(line: &str, content_col: usize) -> bool {
45 line.as_bytes()
46 .get(content_col..content_col + Self::TASK_CHECKBOX_PREFIX_LEN)
47 .is_some_and(|window| matches!(window, b"[ ] " | b"[x] " | b"[X] "))
48 }
49
50 fn is_block_level_construct(trimmed: &str) -> bool {
52 if trimmed.starts_with("[^") && trimmed.contains("]:") {
54 return true;
55 }
56 if trimmed.starts_with("*[") && trimmed.contains("]:") {
58 return true;
59 }
60 if trimmed.starts_with('[') && !trimmed.starts_with("[^") && trimmed.contains("]: ") {
63 return true;
64 }
65 false
66 }
67
68 fn is_code_fence(trimmed: &str) -> bool {
70 let bytes = trimmed.as_bytes();
71 if bytes.len() < 3 {
72 return false;
73 }
74 let ch = bytes[0];
75 (ch == b'`' || ch == b'~') && bytes[1] == ch && bytes[2] == ch
76 }
77
78 fn starts_with_list_marker(trimmed: &str) -> bool {
82 let bytes = trimmed.as_bytes();
83 match bytes.first() {
84 Some(b'*' | b'-' | b'+') => bytes.get(1).is_some_and(|&b| b == b' ' || b == b'\t'),
85 Some(b'0'..=b'9') => {
86 let rest = trimmed.trim_start_matches(|c: char| c.is_ascii_digit());
87 rest.starts_with(". ") || rest.starts_with(") ")
88 }
89 _ => false,
90 }
91 }
92
93 fn find_fence_closer(ctx: &LintContext, opener_line: usize) -> usize {
97 let mut closer_line = opener_line;
98 for peek in (opener_line + 1)..=ctx.lines.len() {
99 let Some(peek_info) = ctx.line_info(peek) else { break };
100 if peek_info.in_code_block {
101 closer_line = peek;
102 } else {
103 break;
104 }
105 }
106 closer_line
107 }
108
109 fn build_compound_fence_fix(
140 ctx: &LintContext,
141 opener_line: usize,
142 closer_line: usize,
143 opener_actual: usize,
144 required: usize,
145 ) -> Option<Fix> {
146 if required <= opener_actual {
147 return None;
148 }
149 let opener_info = ctx.line_info(opener_line)?;
150 let closer_info = ctx.line_info(closer_line)?;
151
152 let fix_start = opener_info.byte_offset;
153 let fix_end = closer_info.byte_offset + closer_info.byte_len;
154
155 let mut replacement = String::new();
156 for i in opener_line..=closer_line {
157 let info = ctx.line_info(i)?;
158 if i > opener_line {
159 replacement.push('\n');
160 }
161 let line = info.content(ctx.content);
162 if info.is_blank {
163 replacement.push_str(line);
165 } else {
166 let new_visual = if i == opener_line || i == closer_line {
167 required
168 } else {
169 info.visual_indent.max(required)
170 };
171 for _ in 0..new_visual {
172 replacement.push(' ');
173 }
174 replacement.push_str(&line[info.indent..]);
175 }
176 }
177
178 Some(Fix {
179 range: fix_start..fix_end,
180 replacement,
181 })
182 }
183
184 fn walk_item_continuation<F>(
207 ctx: &LintContext,
208 item_line: usize,
209 range_end: usize,
210 marker_col: usize,
211 mut per_line: F,
212 ) where
213 F: FnMut(&ContinuationLine<'_>) -> ControlFlow<()>,
214 {
215 let mut saw_blank = false;
216 let mut nested_content_col: Option<usize> = None;
217
218 for line_num in (item_line + 1)..=range_end {
219 let Some(info) = ctx.line_info(line_num) else {
220 continue;
221 };
222
223 let trimmed = info.content(ctx.content).trim_start();
224
225 if Self::should_skip_line(info, trimmed) {
226 continue;
227 }
228
229 if info.is_blank {
230 saw_blank = true;
231 continue;
232 }
233
234 if let Some(ref li) = info.list_item {
235 nested_content_col = (li.marker_column > marker_col).then_some(li.content_column);
236 saw_blank = false;
237 continue;
238 }
239
240 if info.heading.is_some() || info.is_horizontal_rule {
241 break;
242 }
243
244 if Self::is_block_level_construct(trimmed) {
245 continue;
246 }
247
248 let col = info.visual_indent;
249
250 if let Some(ncc) = nested_content_col {
251 if col >= ncc {
252 continue;
253 }
254 nested_content_col = None;
255 }
256
257 if saw_blank && col <= marker_col {
258 break;
259 }
260
261 let line = ContinuationLine {
262 line_num,
263 info,
264 trimmed,
265 actual: col,
266 saw_blank,
267 };
268 if per_line(&line).is_break() {
269 break;
270 }
271 }
272 }
273
274 fn sibling_column_usage(
284 ctx: &LintContext,
285 item_line: usize,
286 range_end: usize,
287 marker_col: usize,
288 content_col: usize,
289 task_col: usize,
290 ) -> (bool, bool) {
291 let mut uses_content = false;
292 let mut uses_task = false;
293
294 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
295 if line.actual == content_col {
296 uses_content = true;
297 }
298 if line.actual == task_col {
299 uses_task = true;
300 }
301 if uses_content && uses_task {
302 ControlFlow::Break(())
303 } else {
304 ControlFlow::Continue(())
305 }
306 });
307
308 (uses_content, uses_task)
309 }
310
311 fn compute_fix_target(
317 actual: usize,
318 required: usize,
319 task_col: Option<usize>,
320 uses_content_col: bool,
321 uses_task_col: bool,
322 ) -> usize {
323 let Some(t) = task_col else { return required };
324 match actual.abs_diff(t).cmp(&actual.abs_diff(required)) {
325 std::cmp::Ordering::Less => t,
326 std::cmp::Ordering::Greater => required,
327 std::cmp::Ordering::Equal => match (uses_task_col, uses_content_col) {
328 (true, false) => t,
329 _ => required,
330 },
331 }
332 }
333
334 fn should_skip_line(info: &crate::lint_context::LineInfo, trimmed: &str) -> bool {
339 if info.in_code_block && !Self::is_code_fence(trimmed) {
340 return true;
341 }
342 info.in_front_matter
343 || info.in_html_block
344 || info.in_html_comment
345 || info.in_mdx_comment
346 || info.in_mkdocstrings
347 || info.in_esm_block
348 || info.in_math_block
349 || info.in_admonition
350 || info.in_content_tab
351 || info.in_pymdown_block
352 || info.in_definition_list
353 || info.in_mkdocs_html_markdown
354 || info.in_kramdown_extension_block
355 }
356
357 fn build_over_indent_warning(
359 ctx: &LintContext,
360 line: &ContinuationLine<'_>,
361 fix_target: usize,
362 message: String,
363 ) -> LintWarning {
364 let line_content = line.info.content(ctx.content);
365 let fix_start = line.info.byte_offset;
366 let fix_end = fix_start + line.info.indent;
367 LintWarning {
368 rule_name: Some("MD077".to_string()),
369 line: line.line_num,
370 column: 1,
371 end_line: line.line_num,
372 end_column: line_content.len() + 1,
373 message,
374 severity: Severity::Warning,
375 fix: Some(Fix {
376 range: fix_start..fix_end,
377 replacement: " ".repeat(fix_target),
378 }),
379 }
380 }
381
382 fn build_under_indent_warning(
394 ctx: &LintContext,
395 line: &ContinuationLine<'_>,
396 required: usize,
397 message: String,
398 ) -> UnderIndentOutcome {
399 let line_content = line.info.content(ctx.content);
400 let is_fence_opener = line.info.in_code_block
401 && Self::is_code_fence(line.trimmed)
402 && ctx.line_info(line.line_num - 1).is_none_or(|p| !p.in_code_block);
403
404 let (fix, warn_end_line, warn_end_column, compound_closer) = if is_fence_opener {
405 let closer_line = Self::find_fence_closer(ctx, line.line_num);
406 let fix = Self::build_compound_fence_fix(ctx, line.line_num, closer_line, line.actual, required);
407 let end_column = ctx
408 .line_info(closer_line)
409 .map_or(line_content.len() + 1, |ci| ci.content(ctx.content).len() + 1);
410 let extra_flag = (closer_line != line.line_num).then_some(closer_line);
411 (fix, closer_line, end_column, extra_flag)
412 } else {
413 let fix_start = line.info.byte_offset;
414 let fix_end = fix_start + line.info.indent;
415 let fix = Some(Fix {
416 range: fix_start..fix_end,
417 replacement: " ".repeat(required),
418 });
419 (fix, line.line_num, line_content.len() + 1, None)
420 };
421
422 UnderIndentOutcome {
423 warning: LintWarning {
424 rule_name: Some("MD077".to_string()),
425 line: line.line_num,
426 column: 1,
427 end_line: warn_end_line,
428 end_column: warn_end_column,
429 message,
430 severity: Severity::Warning,
431 fix,
432 },
433 also_flag_line: compound_closer,
434 }
435 }
436}
437
438struct ContinuationLine<'a> {
442 line_num: usize,
443 info: &'a LineInfo,
444 trimmed: &'a str,
445 actual: usize,
446 saw_blank: bool,
447}
448
449struct UnderIndentOutcome {
454 warning: LintWarning,
455 also_flag_line: Option<usize>,
456}
457
458impl Rule for MD077ListContinuationIndent {
459 fn name(&self) -> &'static str {
460 "MD077"
461 }
462
463 fn description(&self) -> &'static str {
464 "List continuation content indentation"
465 }
466
467 fn check(&self, ctx: &LintContext) -> LintResult {
468 if ctx.content.is_empty() {
469 return Ok(Vec::new());
470 }
471
472 let strict_indent = ctx.flavor.requires_strict_list_indent();
473 let total_lines = ctx.lines.len();
474 let mut warnings = Vec::new();
475 let mut flagged_lines = std::collections::HashSet::new();
476
477 let mut items: Vec<(usize, usize, usize, Option<usize>)> = Vec::new();
486 for block in &ctx.list_blocks {
487 for &item_line in &block.item_lines {
488 if let Some(info) = ctx.line_info(item_line)
489 && let Some(ref li) = info.list_item
490 {
491 let line = info.content(ctx.content);
492 let task_col = Self::is_task_list_item(line, li.content_column)
493 .then_some(li.content_column + Self::TASK_CHECKBOX_PREFIX_LEN);
494 items.push((item_line, li.marker_column, li.content_column, task_col));
495 }
496 }
497 }
498 items.sort_unstable();
499 items.dedup_by_key(|&mut (ln, _, _, _)| ln);
500
501 for (item_idx, &(item_line, marker_col, content_col, task_col)) in items.iter().enumerate() {
502 let required = if strict_indent { content_col.max(4) } else { content_col };
503
504 let range_end = items
507 .iter()
508 .skip(item_idx + 1)
509 .find(|&&(_, mc, _, _)| mc <= marker_col)
510 .map_or(total_lines, |&(ln, _, _, _)| ln - 1);
511
512 let (uses_content_col, uses_task_col) = match task_col {
516 Some(t) => Self::sibling_column_usage(ctx, item_line, range_end, marker_col, content_col, t),
517 None => (false, false),
518 };
519
520 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
521 let actual = line.actual;
522 if !line.saw_blank {
523 if actual > required
525 && Some(actual) != task_col
526 && !Self::starts_with_list_marker(line.trimmed)
527 && flagged_lines.insert(line.line_num)
528 {
529 let fix_target =
530 Self::compute_fix_target(actual, required, task_col, uses_content_col, uses_task_col);
531 let message = match task_col {
532 Some(t) => format!(
533 "Continuation line over-indented \
534 (expected {required} or {t}, found {actual})"
535 ),
536 None => {
537 format!("Continuation line over-indented (expected {required}, found {actual})")
538 }
539 };
540 warnings.push(Self::build_over_indent_warning(ctx, line, fix_target, message));
541 }
542 } else if actual < required && flagged_lines.insert(line.line_num) {
543 let message = if strict_indent {
545 format!(
546 "Content inside list item needs {required} spaces of indentation \
547 for MkDocs compatibility (found {actual})",
548 )
549 } else {
550 format!(
551 "Content after blank line in list item needs {required} spaces of \
552 indentation to remain part of the list (found {actual})",
553 )
554 };
555 let outcome = Self::build_under_indent_warning(ctx, line, required, message);
556 if let Some(closer_line) = outcome.also_flag_line {
557 flagged_lines.insert(closer_line);
558 }
559 warnings.push(outcome.warning);
560 }
561 ControlFlow::Continue(())
562 });
563 }
564
565 Ok(warnings)
566 }
567
568 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
569 let warnings = self.check(ctx)?;
570 let warnings =
571 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
572 if warnings.is_empty() {
573 return Ok(ctx.content.to_string());
574 }
575
576 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
578 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
579
580 let mut content = ctx.content.to_string();
581 for fix in fixes {
582 if fix.range.start <= content.len() && fix.range.end <= content.len() {
583 content.replace_range(fix.range, &fix.replacement);
584 }
585 }
586
587 Ok(content)
588 }
589
590 fn category(&self) -> RuleCategory {
591 RuleCategory::List
592 }
593
594 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
595 ctx.content.is_empty() || ctx.list_blocks.is_empty()
596 }
597
598 fn as_any(&self) -> &dyn std::any::Any {
599 self
600 }
601
602 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
603 where
604 Self: Sized,
605 {
606 Box::new(Self)
607 }
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613 use crate::config::MarkdownFlavor;
614
615 fn check(content: &str) -> Vec<LintWarning> {
616 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
617 let rule = MD077ListContinuationIndent;
618 rule.check(&ctx).unwrap()
619 }
620
621 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
622 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
623 let rule = MD077ListContinuationIndent;
624 rule.check(&ctx).unwrap()
625 }
626
627 fn fix(content: &str) -> String {
628 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
629 let rule = MD077ListContinuationIndent;
630 rule.fix(&ctx).unwrap()
631 }
632
633 fn fix_mkdocs(content: &str) -> String {
634 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
635 let rule = MD077ListContinuationIndent;
636 rule.fix(&ctx).unwrap()
637 }
638
639 #[test]
642 fn tight_lazy_continuation_zero_indent_not_flagged() {
643 let content = "- Item\ncontinuation\n";
645 assert!(check(content).is_empty());
646 }
647
648 #[test]
649 fn tight_continuation_correct_indent_not_flagged() {
650 let content = "1. Item\n continuation\n";
652 assert!(check(content).is_empty());
653 }
654
655 #[test]
656 fn tight_continuation_over_indented_ordered() {
657 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
659 let warnings = check(content);
660 assert_eq!(warnings.len(), 1);
661 assert_eq!(warnings[0].line, 2);
662 assert!(warnings[0].message.contains("over-indented"));
663 }
664
665 #[test]
666 fn tight_continuation_over_indented_unordered() {
667 let content = "- Item\n over-indented\n";
669 let warnings = check(content);
670 assert_eq!(warnings.len(), 1);
671 assert_eq!(warnings[0].line, 2);
672 }
673
674 #[test]
675 fn tight_continuation_multiple_over_indented_lines() {
676 let content = "1. Item\n line one\n line two\n line three\n";
677 let warnings = check(content);
678 assert_eq!(warnings.len(), 3);
679 }
680
681 #[test]
682 fn tight_continuation_mixed_correct_and_over() {
683 let content = "1. Item\n correct\n over-indented\n correct again\n";
684 let warnings = check(content);
685 assert_eq!(warnings.len(), 1);
686 assert_eq!(warnings[0].line, 3);
687 }
688
689 #[test]
690 fn tight_continuation_nested_over_indented() {
691 let content = "- L1\n - L2\n over-indented continuation of L2\n";
693 let warnings = check(content);
694 assert_eq!(warnings.len(), 1);
695 assert_eq!(warnings[0].line, 3);
696 assert!(warnings[0].message.contains("expected 4"));
698 assert!(warnings[0].message.contains("found 5"));
699 }
700
701 #[test]
702 fn tight_continuation_nested_correct_indent_not_flagged() {
703 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
706 assert!(check(content).is_empty());
707 }
708
709 #[test]
710 fn fix_tight_continuation_nested_over_indented() {
711 let content = "- L1\n - L2\n over-indented continuation of L2\n";
713 let fixed = fix(content);
714 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
715 }
716
717 #[test]
718 fn tight_continuation_under_indented_not_flagged() {
719 let content = "1. Item\n under-indented\n";
722 assert!(check(content).is_empty());
723 }
724
725 #[test]
726 fn tight_continuation_tab_over_indented() {
727 let content = "- Item\n\tover-indented\n";
729 let warnings = check(content);
730 assert_eq!(warnings.len(), 1);
731 }
732
733 #[test]
734 fn fix_tight_continuation_over_indented_ordered() {
735 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
736 let fixed = fix(content);
737 assert_eq!(
738 fixed,
739 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
740 );
741 }
742
743 #[test]
744 fn fix_tight_continuation_over_indented_unordered() {
745 let content = "- Item\n over-indented\n";
746 let fixed = fix(content);
747 assert_eq!(fixed, "- Item\n over-indented\n");
748 }
749
750 #[test]
751 fn fix_tight_continuation_multiple_lines() {
752 let content = "1. Item\n line one\n line two\n";
753 let fixed = fix(content);
754 assert_eq!(fixed, "1. Item\n line one\n line two\n");
755 }
756
757 #[test]
758 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
759 let content = "1. Item\n continuation\n";
762 assert!(check_mkdocs(content).is_empty());
763 }
764
765 #[test]
766 fn tight_continuation_mkdocs_5space_ordered_flagged() {
767 let content = "1. Item\n over-indented\n";
769 let warnings = check_mkdocs(content);
770 assert_eq!(warnings.len(), 1);
771 assert!(warnings[0].message.contains("expected 4"));
772 assert!(warnings[0].message.contains("found 5"));
773 }
774
775 #[test]
776 fn fix_tight_continuation_mkdocs_over_indented() {
777 let content = "1. Item\n over-indented\n";
778 let fixed = fix_mkdocs(content);
779 assert_eq!(fixed, "1. Item\n over-indented\n");
780 }
781
782 #[test]
783 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
784 let content = "* Level 0\n * Level 1\n * Level 2\n";
787 assert!(check(content).is_empty());
788 }
789
790 #[test]
791 fn tight_continuation_ordered_marker_not_flagged() {
792 let content = "- Parent\n 1. Child item\n";
794 assert!(check(content).is_empty());
795 }
796
797 #[test]
800 fn unordered_correct_indent_no_warning() {
801 let content = "- Item\n\n continuation\n";
802 assert!(check(content).is_empty());
803 }
804
805 #[test]
806 fn unordered_partial_indent_warns() {
807 let content = "- Item\n\n continuation\n";
810 let warnings = check(content);
811 assert_eq!(warnings.len(), 1);
812 assert_eq!(warnings[0].line, 3);
813 assert!(warnings[0].message.contains("2 spaces"));
814 assert!(warnings[0].message.contains("found 1"));
815 }
816
817 #[test]
818 fn unordered_zero_indent_is_new_paragraph() {
819 let content = "- Item\n\ncontinuation\n";
822 assert!(check(content).is_empty());
823 }
824
825 #[test]
828 fn ordered_3space_correct_commonmark() {
829 let content = "1. Item\n\n continuation\n";
831 assert!(check(content).is_empty());
832 }
833
834 #[test]
835 fn ordered_2space_under_indent_commonmark() {
836 let content = "1. Item\n\n continuation\n";
837 let warnings = check(content);
838 assert_eq!(warnings.len(), 1);
839 assert!(warnings[0].message.contains("3 spaces"));
840 assert!(warnings[0].message.contains("found 2"));
841 }
842
843 #[test]
846 fn multi_digit_marker_correct() {
847 let content = "10. Item\n\n continuation\n";
849 assert!(check(content).is_empty());
850 }
851
852 #[test]
853 fn multi_digit_marker_under_indent() {
854 let content = "10. Item\n\n continuation\n";
855 let warnings = check(content);
856 assert_eq!(warnings.len(), 1);
857 assert!(warnings[0].message.contains("4 spaces"));
858 }
859
860 #[test]
863 fn mkdocs_3space_ordered_warns() {
864 let content = "1. Item\n\n continuation\n";
866 let warnings = check_mkdocs(content);
867 assert_eq!(warnings.len(), 1);
868 assert!(warnings[0].message.contains("4 spaces"));
869 assert!(warnings[0].message.contains("MkDocs"));
870 }
871
872 #[test]
873 fn mkdocs_4space_ordered_no_warning() {
874 let content = "1. Item\n\n continuation\n";
875 assert!(check_mkdocs(content).is_empty());
876 }
877
878 #[test]
879 fn mkdocs_unordered_2space_ok() {
880 let content = "- Item\n\n continuation\n";
882 assert!(check_mkdocs(content).is_empty());
883 }
884
885 #[test]
886 fn mkdocs_unordered_2space_warns() {
887 let content = "- Item\n\n continuation\n";
889 let warnings = check_mkdocs(content);
890 assert_eq!(warnings.len(), 1);
891 assert!(warnings[0].message.contains("4 spaces"));
892 }
893
894 #[test]
897 fn fix_unordered_indent() {
898 let content = "- Item\n\n continuation\n";
900 let fixed = fix(content);
901 assert_eq!(fixed, "- Item\n\n continuation\n");
902 }
903
904 #[test]
905 fn fix_ordered_indent() {
906 let content = "1. Item\n\n continuation\n";
907 let fixed = fix(content);
908 assert_eq!(fixed, "1. Item\n\n continuation\n");
909 }
910
911 #[test]
912 fn fix_mkdocs_indent() {
913 let content = "1. Item\n\n continuation\n";
914 let fixed = fix_mkdocs(content);
915 assert_eq!(fixed, "1. Item\n\n continuation\n");
916 }
917
918 #[test]
921 fn nested_list_items_not_flagged() {
922 let content = "- Parent\n\n - Child\n";
923 assert!(check(content).is_empty());
924 }
925
926 #[test]
927 fn nested_list_zero_indent_is_new_paragraph() {
928 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
930 assert!(check(content).is_empty());
931 }
932
933 #[test]
934 fn nested_list_partial_indent_flagged() {
935 let content = "- Parent\n - Child\n\n continuation of parent\n";
937 let warnings = check(content);
938 assert_eq!(warnings.len(), 1);
939 assert!(warnings[0].message.contains("2 spaces"));
940 }
941
942 #[test]
945 fn code_block_correctly_indented_no_warning() {
946 let content = "- Item\n\n ```\n code\n ```\n";
948 assert!(check(content).is_empty());
949 }
950
951 #[test]
952 fn code_fence_under_indented_warns() {
953 let content = "- Item\n\n ```\n code\n ```\n";
957 let warnings = check(content);
958 assert_eq!(warnings.len(), 1);
959 assert_eq!(warnings[0].line, 3);
960 }
961
962 #[test]
963 fn code_fence_under_indented_ordered_mkdocs() {
964 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
967 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
969 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
971 assert!(warnings[0].message.contains("4 spaces"));
972 assert!(warnings[0].message.contains("MkDocs"));
973 }
974
975 #[test]
976 fn code_fence_tilde_under_indented() {
977 let content = "- Item\n\n ~~~\n code\n ~~~\n";
978 let warnings = check(content);
979 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
981 }
982
983 #[test]
986 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
987 let content = "- Item\n\n\ncontinuation\n";
989 assert!(check(content).is_empty());
990 }
991
992 #[test]
993 fn multiple_blank_lines_partial_indent_flags() {
994 let content = "- Item\n\n\n continuation\n";
995 let warnings = check(content);
996 assert_eq!(warnings.len(), 1);
997 }
998
999 #[test]
1002 fn empty_item_no_warning() {
1003 let content = "- \n- Second\n";
1004 assert!(check(content).is_empty());
1005 }
1006
1007 #[test]
1010 fn multiple_items_mixed_indent() {
1011 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
1012 let warnings = check(content);
1013 assert_eq!(warnings.len(), 1);
1014 assert_eq!(warnings[0].line, 7);
1015 }
1016
1017 #[test]
1020 fn task_list_correct_indent() {
1021 let content = "- [ ] Task\n\n continuation\n";
1023 assert!(check(content).is_empty());
1024 }
1025
1026 #[test]
1029 fn frontmatter_not_flagged() {
1030 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
1031 assert!(check(content).is_empty());
1032 }
1033
1034 #[test]
1037 fn fix_multiple_items() {
1038 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
1039 let fixed = fix(content);
1040 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
1041 }
1042
1043 #[test]
1044 fn fix_multiline_loose_continuation_all_lines() {
1045 let content = "1. Item\n\n line one\n line two\n line three\n";
1046 let fixed = fix(content);
1047 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
1048 }
1049
1050 #[test]
1053 fn sibling_item_boundary_respected() {
1054 let content = "- First\n- Second\n\n continuation\n";
1056 assert!(check(content).is_empty());
1057 }
1058
1059 #[test]
1062 fn blockquote_list_correct_indent_no_warning() {
1063 let content = "> - Item\n>\n> continuation\n";
1066 assert!(check(content).is_empty());
1067 }
1068
1069 #[test]
1070 fn blockquote_list_under_indent_no_false_positive() {
1071 let content = "> - Item\n>\n> continuation\n";
1076 assert!(check(content).is_empty());
1077 }
1078
1079 #[test]
1082 fn deeply_nested_correct_indent() {
1083 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1084 assert!(check(content).is_empty());
1085 }
1086
1087 #[test]
1088 fn deeply_nested_under_indent() {
1089 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1092 let warnings = check(content);
1093 assert_eq!(warnings.len(), 1);
1094 assert!(warnings[0].message.contains("6 spaces"));
1095 assert!(warnings[0].message.contains("found 5"));
1096 }
1097
1098 #[test]
1101 fn tab_indent_correct() {
1102 let content = "- Item\n\n\tcontinuation\n";
1105 assert!(check(content).is_empty());
1106 }
1107
1108 #[test]
1111 fn multiple_continuations_correct() {
1112 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
1113 assert!(check(content).is_empty());
1114 }
1115
1116 #[test]
1117 fn multiple_continuations_second_under_indent() {
1118 let content = "- Item\n\n para 1\n\n continuation 2\n";
1120 let warnings = check(content);
1121 assert_eq!(warnings.len(), 1);
1122 assert_eq!(warnings[0].line, 5);
1123 }
1124
1125 #[test]
1128 fn ordered_paren_marker_correct() {
1129 let content = "1) Item\n\n continuation\n";
1131 assert!(check(content).is_empty());
1132 }
1133
1134 #[test]
1135 fn ordered_paren_marker_under_indent() {
1136 let content = "1) Item\n\n continuation\n";
1137 let warnings = check(content);
1138 assert_eq!(warnings.len(), 1);
1139 assert!(warnings[0].message.contains("3 spaces"));
1140 }
1141
1142 #[test]
1145 fn star_marker_correct() {
1146 let content = "* Item\n\n continuation\n";
1147 assert!(check(content).is_empty());
1148 }
1149
1150 #[test]
1151 fn star_marker_under_indent() {
1152 let content = "* Item\n\n continuation\n";
1153 let warnings = check(content);
1154 assert_eq!(warnings.len(), 1);
1155 }
1156
1157 #[test]
1158 fn plus_marker_correct() {
1159 let content = "+ Item\n\n continuation\n";
1160 assert!(check(content).is_empty());
1161 }
1162
1163 #[test]
1166 fn heading_after_list_no_warning() {
1167 let content = "- Item\n\n# Heading\n";
1168 assert!(check(content).is_empty());
1169 }
1170
1171 #[test]
1174 fn hr_after_list_no_warning() {
1175 let content = "- Item\n\n---\n";
1176 assert!(check(content).is_empty());
1177 }
1178
1179 #[test]
1182 fn reference_link_def_not_flagged() {
1183 let content = "- Item\n\n [link]: https://example.com\n";
1184 assert!(check(content).is_empty());
1185 }
1186
1187 #[test]
1190 fn footnote_def_not_flagged() {
1191 let content = "- Item\n\n [^1]: footnote text\n";
1192 assert!(check(content).is_empty());
1193 }
1194
1195 #[test]
1198 fn fix_deeply_nested() {
1199 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
1200 let fixed = fix(content);
1201 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
1202 }
1203
1204 #[test]
1205 fn fix_mkdocs_unordered() {
1206 let content = "- Item\n\n continuation\n";
1208 let fixed = fix_mkdocs(content);
1209 assert_eq!(fixed, "- Item\n\n continuation\n");
1210 }
1211
1212 #[test]
1213 fn fix_code_fence_indent() {
1214 let content = "- Item\n\n ```\n code\n ```\n";
1217 let fixed = fix(content);
1218 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
1219 }
1220
1221 #[test]
1222 fn fix_mkdocs_code_fence_indent() {
1223 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
1225 let fixed = fix_mkdocs(content);
1226 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
1227 }
1228
1229 #[test]
1232 fn empty_document_no_warning() {
1233 assert!(check("").is_empty());
1234 }
1235
1236 #[test]
1237 fn whitespace_only_no_warning() {
1238 assert!(check(" \n\n \n").is_empty());
1239 }
1240
1241 #[test]
1244 fn no_list_no_warning() {
1245 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
1246 assert!(check(content).is_empty());
1247 }
1248
1249 #[test]
1252 fn multiline_continuation_all_lines_flagged() {
1253 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";
1254 let warnings = check(content);
1255 assert_eq!(warnings.len(), 3);
1256 assert_eq!(warnings[0].line, 3);
1257 assert_eq!(warnings[1].line, 4);
1258 assert_eq!(warnings[2].line, 5);
1259 }
1260
1261 #[test]
1262 fn multiline_continuation_with_frontmatter_fix() {
1263 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";
1264 let fixed = fix(content);
1265 assert_eq!(
1266 fixed,
1267 "---\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"
1268 );
1269 }
1270
1271 #[test]
1272 fn multiline_continuation_correct_indent_no_warning() {
1273 let content = "1. Item\n\n line one\n line two\n line three\n";
1274 assert!(check(content).is_empty());
1275 }
1276
1277 #[test]
1278 fn multiline_continuation_mixed_indent() {
1279 let content = "1. Item\n\n correct\n wrong\n correct\n";
1280 let warnings = check(content);
1281 assert_eq!(warnings.len(), 1);
1282 assert_eq!(warnings[0].line, 4);
1283 }
1284
1285 #[test]
1286 fn multiline_continuation_unordered() {
1287 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
1288 let warnings = check(content);
1289 assert_eq!(warnings.len(), 3);
1290 let fixed = fix(content);
1291 assert_eq!(
1292 fixed,
1293 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
1294 );
1295 }
1296
1297 #[test]
1298 fn multiline_continuation_two_items_fix() {
1299 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
1300 let fixed = fix(content);
1301 assert_eq!(
1302 fixed,
1303 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1304 );
1305 }
1306
1307 #[test]
1308 fn fence_fix_does_not_break_pairing_for_md031() {
1309 let content = "#### title\n\nabc\n\n\
1316 1. ab\n\n\
1317 \x20\x20`aabbccdd`\n\n\
1318 2. cd\n\n\
1319 \x20\x20`bbcc dd ee`\n\n\
1320 \x20\x20```\n\
1321 \x20\x20abcd\n\
1322 \x20\x20ef gh\n\
1323 \x20\x20```\n\n\
1324 \x20\x20uu\n\n\
1325 \x20\x20```\n\
1326 \x20\x20cdef\n\
1327 \x20\x20gh ij\n\
1328 \x20\x20```\n";
1329 let expected = "#### title\n\nabc\n\n\
1330 1. ab\n\n\
1331 \x20\x20\x20`aabbccdd`\n\n\
1332 2. cd\n\n\
1333 \x20\x20\x20`bbcc dd ee`\n\n\
1334 \x20\x20\x20```\n\
1335 \x20\x20\x20abcd\n\
1336 \x20\x20\x20ef gh\n\
1337 \x20\x20\x20```\n\n\
1338 \x20\x20\x20uu\n\n\
1339 \x20\x20\x20```\n\
1340 \x20\x20\x20cdef\n\
1341 \x20\x20\x20gh ij\n\
1342 \x20\x20\x20```\n";
1343 assert_eq!(fix(content), expected);
1344 }
1345
1346 #[test]
1347 fn multiline_continuation_separated_by_blank() {
1348 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1349 let warnings = check(content);
1350 assert_eq!(warnings.len(), 4);
1351 let fixed = fix(content);
1352 assert_eq!(
1353 fixed,
1354 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1355 );
1356 }
1357
1358 #[test]
1359 fn tab_indented_fence_is_normalized_to_spaces() {
1360 let content = "100. ab\n\n\t```\n\tabcd\n\t```\n";
1368 let expected = "100. ab\n\n ```\n abcd\n ```\n";
1369 assert_eq!(fix(content), expected);
1370 }
1371
1372 #[test]
1380 fn task_list_tight_continuation_post_checkbox_reproducer_579() {
1381 let content = "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n";
1384 assert!(check(content).is_empty());
1385 }
1386
1387 #[test]
1388 fn task_list_tight_continuation_dash_unchecked() {
1389 let content = "- [ ] Task\n continuation\n";
1390 assert!(check(content).is_empty());
1391 }
1392
1393 #[test]
1394 fn task_list_tight_continuation_dash_checked_lower() {
1395 let content = "- [x] Task\n continuation\n";
1396 assert!(check(content).is_empty());
1397 }
1398
1399 #[test]
1400 fn task_list_tight_continuation_dash_checked_upper() {
1401 let content = "- [X] Task\n continuation\n";
1402 assert!(check(content).is_empty());
1403 }
1404
1405 #[test]
1406 fn task_list_tight_continuation_star_marker() {
1407 let content = "* [ ] Task\n continuation\n";
1408 assert!(check(content).is_empty());
1409 }
1410
1411 #[test]
1412 fn task_list_tight_continuation_plus_marker() {
1413 let content = "+ [ ] Task\n continuation\n";
1414 assert!(check(content).is_empty());
1415 }
1416
1417 #[test]
1418 fn task_list_tight_continuation_content_column_still_valid() {
1419 let content = "- [ ] Task\n continuation\n";
1422 assert!(check(content).is_empty());
1423 }
1424
1425 #[test]
1426 fn task_list_tight_continuation_between_columns_still_flagged() {
1427 let content = "- [ ] Task\n continuation\n";
1430 let warnings = check(content);
1431 assert_eq!(warnings.len(), 1);
1432 assert!(warnings[0].message.contains("expected 2 or 6"));
1434 assert!(warnings[0].message.contains("found 4"));
1435 }
1436
1437 #[test]
1438 fn task_list_tight_continuation_overshoot_still_flagged() {
1439 let content = "- [ ] Task\n continuation\n";
1441 let warnings = check(content);
1442 assert_eq!(warnings.len(), 1);
1443 assert!(warnings[0].message.contains("expected 2 or 6"));
1444 assert!(warnings[0].message.contains("found 7"));
1445 }
1446
1447 #[test]
1450 fn fix_task_list_overshoot_snaps_to_task_col() {
1451 let content = "- [ ] Task\n continuation\n";
1455 let fixed = fix(content);
1456 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1457 }
1458
1459 #[test]
1460 fn fix_task_list_col_5_snaps_to_task_col() {
1461 let content = "- [ ] Task\n continuation\n";
1463 let fixed = fix(content);
1464 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1465 }
1466
1467 #[test]
1468 fn fix_task_list_col_3_snaps_to_content_col() {
1469 let content = "- [ ] Task\n continuation\n";
1471 let fixed = fix(content);
1472 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1473 }
1474
1475 #[test]
1476 fn fix_task_list_col_4_ties_to_content_col() {
1477 let content = "- [ ] Task\n continuation\n";
1482 let fixed = fix(content);
1483 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1484 }
1485
1486 #[test]
1487 fn fix_task_list_ordered_overshoot_snaps_to_task_col() {
1488 let content = "1. [ ] Task\n continuation\n";
1491 let fixed = fix(content);
1492 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1493 }
1494
1495 #[test]
1496 fn fix_task_list_ordered_under_overshoot_snaps_to_content_col() {
1497 let content = "1. [ ] Task\n continuation\n";
1500 let fixed = fix(content);
1501 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1502 }
1503
1504 #[test]
1505 fn task_list_tight_continuation_ordered_single_digit() {
1506 let content = "1. [ ] Task\n continuation\n";
1508 assert!(check(content).is_empty());
1509 }
1510
1511 #[test]
1512 fn task_list_tight_continuation_ordered_multi_digit() {
1513 let content = "10. [ ] Task\n continuation\n";
1515 assert!(check(content).is_empty());
1516 }
1517
1518 #[test]
1519 fn task_list_tight_continuation_nested_dash() {
1520 let content = "- Parent\n - [ ] Nested task\n continuation\n";
1522 assert!(check(content).is_empty());
1523 }
1524
1525 #[test]
1526 fn task_list_loose_continuation_post_checkbox_column_not_flagged() {
1527 let content = "- [ ] Task\n\n continuation\n";
1532 assert!(check(content).is_empty());
1533 }
1534
1535 #[test]
1536 fn task_list_empty_body_is_not_a_task() {
1537 let content = "- [ ]\n continuation\n";
1543 let warnings = check(content);
1544 assert_eq!(warnings.len(), 1);
1545 assert!(warnings[0].message.contains("found 4"));
1546 }
1547
1548 #[test]
1549 fn task_list_malformed_checkbox_is_not_a_task() {
1550 let content = "- [~] Not a task\n continuation\n";
1552 let warnings = check(content);
1553 assert_eq!(warnings.len(), 1);
1554 }
1555
1556 #[test]
1563 fn task_list_mkdocs_unordered_required_min_valid() {
1564 let content = "- [ ] Task\n continuation\n";
1566 assert!(check_mkdocs(content).is_empty());
1567 }
1568
1569 #[test]
1570 fn task_list_mkdocs_unordered_post_checkbox_valid() {
1571 let content = "- [ ] Task\n continuation\n";
1572 assert!(check_mkdocs(content).is_empty());
1573 }
1574
1575 #[test]
1576 fn task_list_mkdocs_unordered_between_flagged() {
1577 let content = "- [ ] Task\n continuation\n";
1579 let warnings = check_mkdocs(content);
1580 assert_eq!(warnings.len(), 1);
1581 }
1582
1583 #[test]
1584 fn task_list_mkdocs_ordered_both_columns_valid() {
1585 let at_4 = "1. [ ] Task\n continuation\n";
1587 assert!(check_mkdocs(at_4).is_empty());
1588 let at_7 = "1. [ ] Task\n continuation\n";
1589 assert!(check_mkdocs(at_7).is_empty());
1590 }
1591
1592 #[test]
1593 fn task_list_mkdocs_ordered_between_flagged() {
1594 let at_5 = "1. [ ] Task\n continuation\n";
1596 assert_eq!(check_mkdocs(at_5).len(), 1);
1597 let at_6 = "1. [ ] Task\n continuation\n";
1598 assert_eq!(check_mkdocs(at_6).len(), 1);
1599 }
1600
1601 #[test]
1611 fn fix_task_list_tie_sibling_at_task_col_snaps_to_task_col() {
1612 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
1616 let fixed = fix(content);
1617 assert_eq!(
1618 fixed,
1619 "- [ ] Task\n aligned continuation\n tied continuation\n"
1620 );
1621 }
1622
1623 #[test]
1624 fn fix_task_list_tie_sibling_at_content_col_snaps_to_content_col() {
1625 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
1628 let fixed = fix(content);
1629 assert_eq!(fixed, "- [ ] Task\n aligned continuation\n tied continuation\n");
1630 }
1631
1632 #[test]
1633 fn fix_task_list_tie_both_siblings_snaps_to_content_col() {
1634 let content = "- [ ] Task\n at content col\n at task col\n tied continuation\n";
1638 let fixed = fix(content);
1639 assert_eq!(
1640 fixed,
1641 "- [ ] Task\n at content col\n at task col\n tied continuation\n"
1642 );
1643 }
1644
1645 #[test]
1646 fn fix_task_list_tie_sees_task_col_through_tight_lazy_continuation() {
1647 let content = concat!("- [ ] Task\n", "lazy\n", " aligned at task col\n", " tied\n",);
1661 let fixed = fix(content);
1662 assert!(
1663 fixed.contains("\n tied\n"),
1664 "tied line should snap to col 6 (task col) because a task-col \
1665 sibling is visible past the tight lazy-continuation line; got:\n{fixed}"
1666 );
1667 }
1668
1669 #[test]
1676 fn task_list_tab_indented_continuation_flagged() {
1677 let content = "- [ ] Task\n\t\twrap\n";
1680 let warnings = check(content);
1681 assert_eq!(warnings.len(), 1);
1682 assert!(warnings[0].message.contains("expected 2 or 6"));
1683 assert!(warnings[0].message.contains("found 8"));
1684 }
1685
1686 #[test]
1687 fn fix_task_list_tab_indented_snaps_to_task_col() {
1688 let content = "- [ ] Task\n\t\twrap\n";
1690 let fixed = fix(content);
1691 assert_eq!(fixed, "- [ ] Task\n wrap\n");
1692 }
1693
1694 #[test]
1695 fn fix_task_list_single_tab_equidistant_snaps_to_content_col() {
1696 let content = "- [ ] Task\n\twrap\n";
1699 let fixed = fix(content);
1700 assert_eq!(fixed, "- [ ] Task\n wrap\n");
1701 }
1702
1703 #[test]
1713 fn task_list_blockquote_post_checkbox_not_flagged() {
1714 let content = "> - [ ] Task\n> continuation\n";
1716 assert!(check(content).is_empty());
1717 }
1718
1719 #[test]
1720 fn task_list_blockquote_between_cols_documented_limitation() {
1721 let content = "> - [ ] Task\n> continuation\n";
1725 assert!(check(content).is_empty());
1726 }
1727
1728 #[test]
1729 fn task_list_blockquote_overshoot_documented_limitation() {
1730 let content = "> - [ ] Task\n> continuation\n";
1732 assert!(check(content).is_empty());
1733 }
1734
1735 #[test]
1742 fn fix_task_list_mkdocs_unordered_overshoot_snaps_to_task_col() {
1743 let content = "- [ ] Task\n continuation\n";
1746 let fixed = fix_mkdocs(content);
1747 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1748 }
1749
1750 #[test]
1751 fn fix_task_list_mkdocs_unordered_tie_snaps_to_required() {
1752 let content = "- [ ] Task\n continuation\n";
1755 let fixed = fix_mkdocs(content);
1756 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1757 }
1758
1759 #[test]
1760 fn fix_task_list_mkdocs_ordered_overshoot_snaps_to_task_col() {
1761 let content = "1. [ ] Task\n continuation\n";
1764 let fixed = fix_mkdocs(content);
1765 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1766 }
1767
1768 #[test]
1769 fn fix_task_list_mkdocs_ordered_near_required_snaps_to_required() {
1770 let content = "1. [ ] Task\n continuation\n";
1776 let fixed = fix_mkdocs(content);
1777 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1778 }
1779
1780 #[test]
1781 fn fix_task_list_mkdocs_ordered_between_cols_snaps_to_task_col() {
1782 let content = "1. [ ] Task\n continuation\n";
1785 let fixed = fix_mkdocs(content);
1786 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1787 }
1788
1789 fn assert_idempotent(content: &str) {
1799 let once = fix(content);
1800 let twice = fix(&once);
1801 assert_eq!(once, twice, "MD077 fix was not idempotent on input: {content:?}");
1802 }
1803
1804 fn assert_idempotent_mkdocs(content: &str) {
1805 let once = fix_mkdocs(content);
1806 let twice = fix_mkdocs(&once);
1807 assert_eq!(
1808 once, twice,
1809 "MD077 (MkDocs) fix was not idempotent on input: {content:?}"
1810 );
1811 }
1812
1813 #[test]
1814 fn idempotent_task_list_between_cols() {
1815 assert_idempotent("- [ ] Task\n continuation\n");
1816 }
1817
1818 #[test]
1819 fn idempotent_task_list_overshoot() {
1820 assert_idempotent("- [ ] Task\n continuation\n");
1821 }
1822
1823 #[test]
1824 fn idempotent_task_list_under_post_checkbox() {
1825 assert_idempotent("- [ ] Task\n continuation\n");
1826 }
1827
1828 #[test]
1829 fn idempotent_task_list_near_post_checkbox() {
1830 assert_idempotent("- [ ] Task\n continuation\n");
1831 }
1832
1833 #[test]
1834 fn idempotent_task_list_tab_overshoot() {
1835 assert_idempotent("- [ ] Task\n\t\twrap\n");
1836 }
1837
1838 #[test]
1839 fn idempotent_task_list_single_tab() {
1840 assert_idempotent("- [ ] Task\n\twrap\n");
1841 }
1842
1843 #[test]
1844 fn idempotent_task_list_ordered_overshoot() {
1845 assert_idempotent("1. [ ] Task\n continuation\n");
1846 }
1847
1848 #[test]
1849 fn idempotent_task_list_ordered_under() {
1850 assert_idempotent("1. [ ] Task\n continuation\n");
1851 }
1852
1853 #[test]
1854 fn idempotent_task_list_tie_with_sibling_at_task_col() {
1855 assert_idempotent("- [ ] Task\n aligned\n tied\n");
1856 }
1857
1858 #[test]
1859 fn idempotent_task_list_tie_with_sibling_at_content_col() {
1860 assert_idempotent("- [ ] Task\n aligned\n tied\n");
1861 }
1862
1863 #[test]
1864 fn idempotent_task_list_mkdocs_unordered_overshoot() {
1865 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
1866 }
1867
1868 #[test]
1869 fn idempotent_task_list_mkdocs_unordered_tie() {
1870 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
1871 }
1872
1873 #[test]
1874 fn idempotent_task_list_mkdocs_ordered_overshoot() {
1875 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
1876 }
1877
1878 #[test]
1879 fn idempotent_task_list_mkdocs_ordered_between() {
1880 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
1881 }
1882
1883 #[test]
1884 fn idempotent_task_list_reproducer_579() {
1885 assert_idempotent(
1889 "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n",
1890 );
1891 }
1892
1893 #[test]
1894 fn idempotent_non_task_list_still_holds() {
1895 assert_idempotent("1. Item\n over-indented\n");
1898 assert_idempotent("- Item\n\n continuation\n");
1899 }
1900
1901 #[test]
1908 fn idempotent_non_task_loose_under_indent_ordered() {
1909 assert_idempotent("1. Item\n\n continuation\n");
1911 }
1912
1913 #[test]
1914 fn idempotent_non_task_loose_under_indent_multi_digit() {
1915 assert_idempotent("10. Item\n\n continuation\n");
1917 }
1918
1919 #[test]
1920 fn idempotent_non_task_tight_over_indent_ordered() {
1921 assert_idempotent("1. Item\n over-indented\n");
1923 }
1924
1925 #[test]
1933 fn idempotent_non_task_fence_ordered_loose() {
1934 assert_idempotent("1. Item\n\n ```rust\n let x = 1;\n ```\n");
1936 }
1937
1938 #[test]
1939 fn idempotent_non_task_fence_tilde_under_indent() {
1940 assert_idempotent("1. Item\n\n ~~~\nplain text\n ~~~\n");
1946 }
1947
1948 #[test]
1949 fn idempotent_non_task_fence_interior_above_required() {
1950 assert_idempotent("1. Item\n\n ```\n deeply indented code\n ```\n");
1954 }
1955
1956 #[test]
1957 fn fence_fix_promotes_interior_below_scope_in_single_pass() {
1958 let content = "1. Item\n\n ```\ncode\n ```\n";
1962 let fixed = fix(content);
1963 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
1964 }
1965
1966 #[test]
1967 fn fence_fix_preserves_interior_above_required() {
1968 let content = "1. Item\n\n ```\n code\n ```\n";
1971 let fixed = fix(content);
1972 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
1973 }
1974
1975 #[test]
1982 fn idempotent_non_task_mkdocs_ordered_at_3_spaces() {
1983 assert_idempotent_mkdocs("1. Item\n\n continuation\n");
1985 }
1986
1987 #[test]
1988 fn idempotent_non_task_mkdocs_unordered_at_2_spaces() {
1989 assert_idempotent_mkdocs("- Item\n\n continuation\n");
1991 }
1992
1993 #[test]
1994 fn idempotent_non_task_mkdocs_fence_compound() {
1995 assert_idempotent_mkdocs("1. Item\n\n ```toml\n k = 1\n ```\n");
1997 }
1998}