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::new(fix_start..fix_end, replacement))
179 }
180
181 fn walk_item_continuation<F>(
204 ctx: &LintContext,
205 item_line: usize,
206 range_end: usize,
207 marker_col: usize,
208 mut per_line: F,
209 ) where
210 F: FnMut(&ContinuationLine<'_>) -> ControlFlow<()>,
211 {
212 let mut saw_blank = false;
213 let mut nested_content_col: Option<usize> = None;
214
215 for line_num in (item_line + 1)..=range_end {
216 let Some(info) = ctx.line_info(line_num) else {
217 continue;
218 };
219
220 let trimmed = info.content(ctx.content).trim_start();
221
222 if Self::should_skip_line(info, trimmed) {
223 continue;
224 }
225
226 if info.is_blank {
227 saw_blank = true;
228 continue;
229 }
230
231 if let Some(ref li) = info.list_item {
232 nested_content_col = (li.marker_column > marker_col).then_some(li.content_column);
233 saw_blank = false;
234 continue;
235 }
236
237 if info.heading.is_some() || info.is_horizontal_rule {
238 break;
239 }
240
241 if Self::is_block_level_construct(trimmed) {
242 continue;
243 }
244
245 let col = info.visual_indent;
246
247 if let Some(ncc) = nested_content_col {
248 if col >= ncc {
249 continue;
250 }
251 nested_content_col = None;
252 }
253
254 if saw_blank && col <= marker_col {
255 break;
256 }
257
258 let line = ContinuationLine {
259 line_num,
260 info,
261 trimmed,
262 actual: col,
263 saw_blank,
264 };
265 if per_line(&line).is_break() {
266 break;
267 }
268 }
269 }
270
271 fn sibling_column_usage(
281 ctx: &LintContext,
282 item_line: usize,
283 range_end: usize,
284 marker_col: usize,
285 content_col: usize,
286 task_col: usize,
287 ) -> (bool, bool) {
288 let mut uses_content = false;
289 let mut uses_task = false;
290
291 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
292 if line.actual == content_col {
293 uses_content = true;
294 }
295 if line.actual == task_col {
296 uses_task = true;
297 }
298 if uses_content && uses_task {
299 ControlFlow::Break(())
300 } else {
301 ControlFlow::Continue(())
302 }
303 });
304
305 (uses_content, uses_task)
306 }
307
308 fn compute_fix_target(
314 actual: usize,
315 required: usize,
316 task_col: Option<usize>,
317 uses_content_col: bool,
318 uses_task_col: bool,
319 ) -> usize {
320 let Some(t) = task_col else { return required };
321 match actual.abs_diff(t).cmp(&actual.abs_diff(required)) {
322 std::cmp::Ordering::Less => t,
323 std::cmp::Ordering::Greater => required,
324 std::cmp::Ordering::Equal => match (uses_task_col, uses_content_col) {
325 (true, false) => t,
326 _ => required,
327 },
328 }
329 }
330
331 fn should_skip_line(info: &crate::lint_context::LineInfo, trimmed: &str) -> bool {
336 if info.in_code_block && !Self::is_code_fence(trimmed) {
337 return true;
338 }
339 info.in_front_matter
340 || info.in_html_block
341 || info.in_html_comment
342 || info.in_mdx_comment
343 || info.in_mkdocstrings
344 || info.in_esm_block
345 || info.in_math_block
346 || info.in_admonition
347 || info.in_content_tab
348 || info.in_pymdown_block
349 || info.in_definition_list
350 || info.in_mkdocs_html_markdown
351 || info.in_kramdown_extension_block
352 }
353
354 fn build_over_indent_warning(
356 ctx: &LintContext,
357 line: &ContinuationLine<'_>,
358 fix_target: usize,
359 message: String,
360 ) -> LintWarning {
361 let line_content = line.info.content(ctx.content);
362 let fix_start = line.info.byte_offset;
363 let fix_end = fix_start + line.info.indent;
364 LintWarning {
365 rule_name: Some("MD077".to_string()),
366 line: line.line_num,
367 column: 1,
368 end_line: line.line_num,
369 end_column: line_content.len() + 1,
370 message,
371 severity: Severity::Warning,
372 fix: Some(Fix::new(fix_start..fix_end, " ".repeat(fix_target))),
373 }
374 }
375
376 fn build_under_indent_warning(
388 ctx: &LintContext,
389 line: &ContinuationLine<'_>,
390 required: usize,
391 message: String,
392 ) -> UnderIndentOutcome {
393 let line_content = line.info.content(ctx.content);
394 let is_fence_opener = line.info.in_code_block
395 && Self::is_code_fence(line.trimmed)
396 && ctx.line_info(line.line_num - 1).is_none_or(|p| !p.in_code_block);
397
398 let (fix, warn_end_line, warn_end_column, compound_closer) = if is_fence_opener {
399 let closer_line = Self::find_fence_closer(ctx, line.line_num);
400 let fix = Self::build_compound_fence_fix(ctx, line.line_num, closer_line, line.actual, required);
401 let end_column = ctx
402 .line_info(closer_line)
403 .map_or(line_content.len() + 1, |ci| ci.content(ctx.content).len() + 1);
404 let extra_flag = (closer_line != line.line_num).then_some(closer_line);
405 (fix, closer_line, end_column, extra_flag)
406 } else {
407 let fix_start = line.info.byte_offset;
408 let fix_end = fix_start + line.info.indent;
409 let fix = Some(Fix::new(fix_start..fix_end, " ".repeat(required)));
410 (fix, line.line_num, line_content.len() + 1, None)
411 };
412
413 UnderIndentOutcome {
414 warning: LintWarning {
415 rule_name: Some("MD077".to_string()),
416 line: line.line_num,
417 column: 1,
418 end_line: warn_end_line,
419 end_column: warn_end_column,
420 message,
421 severity: Severity::Warning,
422 fix,
423 },
424 also_flag_line: compound_closer,
425 }
426 }
427}
428
429struct ContinuationLine<'a> {
433 line_num: usize,
434 info: &'a LineInfo,
435 trimmed: &'a str,
436 actual: usize,
437 saw_blank: bool,
438}
439
440struct UnderIndentOutcome {
445 warning: LintWarning,
446 also_flag_line: Option<usize>,
447}
448
449impl Rule for MD077ListContinuationIndent {
450 fn name(&self) -> &'static str {
451 "MD077"
452 }
453
454 fn description(&self) -> &'static str {
455 "List continuation content indentation"
456 }
457
458 fn check(&self, ctx: &LintContext) -> LintResult {
459 if ctx.content.is_empty() {
460 return Ok(Vec::new());
461 }
462
463 let strict_indent = ctx.flavor.requires_strict_list_indent();
464 let total_lines = ctx.lines.len();
465 let mut warnings = Vec::new();
466 let mut flagged_lines = std::collections::HashSet::new();
467
468 let mut items: Vec<(usize, usize, usize, Option<usize>)> = Vec::new();
477 for block in &ctx.list_blocks {
478 for &item_line in &block.item_lines {
479 if let Some(info) = ctx.line_info(item_line)
480 && let Some(ref li) = info.list_item
481 {
482 let line = info.content(ctx.content);
483 let task_col = Self::is_task_list_item(line, li.content_column)
484 .then_some(li.content_column + Self::TASK_CHECKBOX_PREFIX_LEN);
485 items.push((item_line, li.marker_column, li.content_column, task_col));
486 }
487 }
488 }
489 items.sort_unstable();
490 items.dedup_by_key(|&mut (ln, _, _, _)| ln);
491
492 for (item_idx, &(item_line, marker_col, content_col, task_col)) in items.iter().enumerate() {
493 let required = if strict_indent { content_col.max(4) } else { content_col };
494
495 let range_end = items
498 .iter()
499 .skip(item_idx + 1)
500 .find(|&&(_, mc, _, _)| mc <= marker_col)
501 .map_or(total_lines, |&(ln, _, _, _)| ln - 1);
502
503 let (uses_content_col, uses_task_col) = match task_col {
507 Some(t) => Self::sibling_column_usage(ctx, item_line, range_end, marker_col, content_col, t),
508 None => (false, false),
509 };
510
511 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
512 let actual = line.actual;
513 if !line.saw_blank {
514 if actual > required
516 && Some(actual) != task_col
517 && !Self::starts_with_list_marker(line.trimmed)
518 && flagged_lines.insert(line.line_num)
519 {
520 let fix_target =
521 Self::compute_fix_target(actual, required, task_col, uses_content_col, uses_task_col);
522 let message = match task_col {
523 Some(t) => format!(
524 "Continuation line over-indented \
525 (expected {required} or {t}, found {actual})"
526 ),
527 None => {
528 format!("Continuation line over-indented (expected {required}, found {actual})")
529 }
530 };
531 warnings.push(Self::build_over_indent_warning(ctx, line, fix_target, message));
532 }
533 } else if actual < required && flagged_lines.insert(line.line_num) {
534 let message = if strict_indent {
536 format!(
537 "Content inside list item needs {required} spaces of indentation \
538 for MkDocs compatibility (found {actual})",
539 )
540 } else {
541 format!(
542 "Content after blank line in list item needs {required} spaces of \
543 indentation to remain part of the list (found {actual})",
544 )
545 };
546 let outcome = Self::build_under_indent_warning(ctx, line, required, message);
547 if let Some(closer_line) = outcome.also_flag_line {
548 flagged_lines.insert(closer_line);
549 }
550 warnings.push(outcome.warning);
551 }
552 ControlFlow::Continue(())
553 });
554 }
555
556 Ok(warnings)
557 }
558
559 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
560 let warnings = self.check(ctx)?;
561 let warnings =
562 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
563 if warnings.is_empty() {
564 return Ok(ctx.content.to_string());
565 }
566
567 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
569 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
570
571 let mut content = ctx.content.to_string();
572 for fix in fixes {
573 if fix.range.start <= content.len() && fix.range.end <= content.len() {
574 content.replace_range(fix.range, &fix.replacement);
575 }
576 }
577
578 Ok(content)
579 }
580
581 fn category(&self) -> RuleCategory {
582 RuleCategory::List
583 }
584
585 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
586 ctx.content.is_empty() || ctx.list_blocks.is_empty()
587 }
588
589 fn as_any(&self) -> &dyn std::any::Any {
590 self
591 }
592
593 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
594 where
595 Self: Sized,
596 {
597 Box::new(Self)
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use crate::config::MarkdownFlavor;
605
606 fn check(content: &str) -> Vec<LintWarning> {
607 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
608 let rule = MD077ListContinuationIndent;
609 rule.check(&ctx).unwrap()
610 }
611
612 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
613 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
614 let rule = MD077ListContinuationIndent;
615 rule.check(&ctx).unwrap()
616 }
617
618 fn fix(content: &str) -> String {
619 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
620 let rule = MD077ListContinuationIndent;
621 rule.fix(&ctx).unwrap()
622 }
623
624 fn fix_mkdocs(content: &str) -> String {
625 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
626 let rule = MD077ListContinuationIndent;
627 rule.fix(&ctx).unwrap()
628 }
629
630 #[test]
633 fn tight_lazy_continuation_zero_indent_not_flagged() {
634 let content = "- Item\ncontinuation\n";
636 assert!(check(content).is_empty());
637 }
638
639 #[test]
640 fn tight_continuation_correct_indent_not_flagged() {
641 let content = "1. Item\n continuation\n";
643 assert!(check(content).is_empty());
644 }
645
646 #[test]
647 fn tight_continuation_over_indented_ordered() {
648 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
650 let warnings = check(content);
651 assert_eq!(warnings.len(), 1);
652 assert_eq!(warnings[0].line, 2);
653 assert!(warnings[0].message.contains("over-indented"));
654 }
655
656 #[test]
657 fn tight_continuation_over_indented_unordered() {
658 let content = "- Item\n over-indented\n";
660 let warnings = check(content);
661 assert_eq!(warnings.len(), 1);
662 assert_eq!(warnings[0].line, 2);
663 }
664
665 #[test]
666 fn tight_continuation_multiple_over_indented_lines() {
667 let content = "1. Item\n line one\n line two\n line three\n";
668 let warnings = check(content);
669 assert_eq!(warnings.len(), 3);
670 }
671
672 #[test]
673 fn tight_continuation_mixed_correct_and_over() {
674 let content = "1. Item\n correct\n over-indented\n correct again\n";
675 let warnings = check(content);
676 assert_eq!(warnings.len(), 1);
677 assert_eq!(warnings[0].line, 3);
678 }
679
680 #[test]
681 fn tight_continuation_nested_over_indented() {
682 let content = "- L1\n - L2\n over-indented continuation of L2\n";
684 let warnings = check(content);
685 assert_eq!(warnings.len(), 1);
686 assert_eq!(warnings[0].line, 3);
687 assert!(warnings[0].message.contains("expected 4"));
689 assert!(warnings[0].message.contains("found 5"));
690 }
691
692 #[test]
693 fn tight_continuation_nested_correct_indent_not_flagged() {
694 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
697 assert!(check(content).is_empty());
698 }
699
700 #[test]
701 fn fix_tight_continuation_nested_over_indented() {
702 let content = "- L1\n - L2\n over-indented continuation of L2\n";
704 let fixed = fix(content);
705 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
706 }
707
708 #[test]
709 fn tight_continuation_under_indented_not_flagged() {
710 let content = "1. Item\n under-indented\n";
713 assert!(check(content).is_empty());
714 }
715
716 #[test]
717 fn tight_continuation_tab_over_indented() {
718 let content = "- Item\n\tover-indented\n";
720 let warnings = check(content);
721 assert_eq!(warnings.len(), 1);
722 }
723
724 #[test]
725 fn fix_tight_continuation_over_indented_ordered() {
726 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
727 let fixed = fix(content);
728 assert_eq!(
729 fixed,
730 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
731 );
732 }
733
734 #[test]
735 fn fix_tight_continuation_over_indented_unordered() {
736 let content = "- Item\n over-indented\n";
737 let fixed = fix(content);
738 assert_eq!(fixed, "- Item\n over-indented\n");
739 }
740
741 #[test]
742 fn fix_tight_continuation_multiple_lines() {
743 let content = "1. Item\n line one\n line two\n";
744 let fixed = fix(content);
745 assert_eq!(fixed, "1. Item\n line one\n line two\n");
746 }
747
748 #[test]
749 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
750 let content = "1. Item\n continuation\n";
753 assert!(check_mkdocs(content).is_empty());
754 }
755
756 #[test]
757 fn tight_continuation_mkdocs_5space_ordered_flagged() {
758 let content = "1. Item\n over-indented\n";
760 let warnings = check_mkdocs(content);
761 assert_eq!(warnings.len(), 1);
762 assert!(warnings[0].message.contains("expected 4"));
763 assert!(warnings[0].message.contains("found 5"));
764 }
765
766 #[test]
767 fn fix_tight_continuation_mkdocs_over_indented() {
768 let content = "1. Item\n over-indented\n";
769 let fixed = fix_mkdocs(content);
770 assert_eq!(fixed, "1. Item\n over-indented\n");
771 }
772
773 #[test]
774 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
775 let content = "* Level 0\n * Level 1\n * Level 2\n";
778 assert!(check(content).is_empty());
779 }
780
781 #[test]
782 fn tight_continuation_ordered_marker_not_flagged() {
783 let content = "- Parent\n 1. Child item\n";
785 assert!(check(content).is_empty());
786 }
787
788 #[test]
791 fn unordered_correct_indent_no_warning() {
792 let content = "- Item\n\n continuation\n";
793 assert!(check(content).is_empty());
794 }
795
796 #[test]
797 fn unordered_partial_indent_warns() {
798 let content = "- Item\n\n continuation\n";
801 let warnings = check(content);
802 assert_eq!(warnings.len(), 1);
803 assert_eq!(warnings[0].line, 3);
804 assert!(warnings[0].message.contains("2 spaces"));
805 assert!(warnings[0].message.contains("found 1"));
806 }
807
808 #[test]
809 fn unordered_zero_indent_is_new_paragraph() {
810 let content = "- Item\n\ncontinuation\n";
813 assert!(check(content).is_empty());
814 }
815
816 #[test]
819 fn ordered_3space_correct_commonmark() {
820 let content = "1. Item\n\n continuation\n";
822 assert!(check(content).is_empty());
823 }
824
825 #[test]
826 fn ordered_2space_under_indent_commonmark() {
827 let content = "1. Item\n\n continuation\n";
828 let warnings = check(content);
829 assert_eq!(warnings.len(), 1);
830 assert!(warnings[0].message.contains("3 spaces"));
831 assert!(warnings[0].message.contains("found 2"));
832 }
833
834 #[test]
837 fn multi_digit_marker_correct() {
838 let content = "10. Item\n\n continuation\n";
840 assert!(check(content).is_empty());
841 }
842
843 #[test]
844 fn multi_digit_marker_under_indent() {
845 let content = "10. Item\n\n continuation\n";
846 let warnings = check(content);
847 assert_eq!(warnings.len(), 1);
848 assert!(warnings[0].message.contains("4 spaces"));
849 }
850
851 #[test]
854 fn mkdocs_3space_ordered_warns() {
855 let content = "1. Item\n\n continuation\n";
857 let warnings = check_mkdocs(content);
858 assert_eq!(warnings.len(), 1);
859 assert!(warnings[0].message.contains("4 spaces"));
860 assert!(warnings[0].message.contains("MkDocs"));
861 }
862
863 #[test]
864 fn mkdocs_4space_ordered_no_warning() {
865 let content = "1. Item\n\n continuation\n";
866 assert!(check_mkdocs(content).is_empty());
867 }
868
869 #[test]
870 fn mkdocs_unordered_2space_ok() {
871 let content = "- Item\n\n continuation\n";
873 assert!(check_mkdocs(content).is_empty());
874 }
875
876 #[test]
877 fn mkdocs_unordered_2space_warns() {
878 let content = "- Item\n\n continuation\n";
880 let warnings = check_mkdocs(content);
881 assert_eq!(warnings.len(), 1);
882 assert!(warnings[0].message.contains("4 spaces"));
883 }
884
885 #[test]
888 fn fix_unordered_indent() {
889 let content = "- Item\n\n continuation\n";
891 let fixed = fix(content);
892 assert_eq!(fixed, "- Item\n\n continuation\n");
893 }
894
895 #[test]
896 fn fix_ordered_indent() {
897 let content = "1. Item\n\n continuation\n";
898 let fixed = fix(content);
899 assert_eq!(fixed, "1. Item\n\n continuation\n");
900 }
901
902 #[test]
903 fn fix_mkdocs_indent() {
904 let content = "1. Item\n\n continuation\n";
905 let fixed = fix_mkdocs(content);
906 assert_eq!(fixed, "1. Item\n\n continuation\n");
907 }
908
909 #[test]
912 fn nested_list_items_not_flagged() {
913 let content = "- Parent\n\n - Child\n";
914 assert!(check(content).is_empty());
915 }
916
917 #[test]
918 fn nested_list_zero_indent_is_new_paragraph() {
919 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
921 assert!(check(content).is_empty());
922 }
923
924 #[test]
925 fn nested_list_partial_indent_flagged() {
926 let content = "- Parent\n - Child\n\n continuation of parent\n";
928 let warnings = check(content);
929 assert_eq!(warnings.len(), 1);
930 assert!(warnings[0].message.contains("2 spaces"));
931 }
932
933 #[test]
936 fn code_block_correctly_indented_no_warning() {
937 let content = "- Item\n\n ```\n code\n ```\n";
939 assert!(check(content).is_empty());
940 }
941
942 #[test]
943 fn code_fence_under_indented_warns() {
944 let content = "- Item\n\n ```\n code\n ```\n";
948 let warnings = check(content);
949 assert_eq!(warnings.len(), 1);
950 assert_eq!(warnings[0].line, 3);
951 }
952
953 #[test]
954 fn code_fence_under_indented_ordered_mkdocs() {
955 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
958 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
960 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
962 assert!(warnings[0].message.contains("4 spaces"));
963 assert!(warnings[0].message.contains("MkDocs"));
964 }
965
966 #[test]
967 fn code_fence_tilde_under_indented() {
968 let content = "- Item\n\n ~~~\n code\n ~~~\n";
969 let warnings = check(content);
970 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
972 }
973
974 #[test]
977 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
978 let content = "- Item\n\n\ncontinuation\n";
980 assert!(check(content).is_empty());
981 }
982
983 #[test]
984 fn multiple_blank_lines_partial_indent_flags() {
985 let content = "- Item\n\n\n continuation\n";
986 let warnings = check(content);
987 assert_eq!(warnings.len(), 1);
988 }
989
990 #[test]
993 fn empty_item_no_warning() {
994 let content = "- \n- Second\n";
995 assert!(check(content).is_empty());
996 }
997
998 #[test]
1001 fn multiple_items_mixed_indent() {
1002 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
1003 let warnings = check(content);
1004 assert_eq!(warnings.len(), 1);
1005 assert_eq!(warnings[0].line, 7);
1006 }
1007
1008 #[test]
1011 fn task_list_correct_indent() {
1012 let content = "- [ ] Task\n\n continuation\n";
1014 assert!(check(content).is_empty());
1015 }
1016
1017 #[test]
1020 fn frontmatter_not_flagged() {
1021 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
1022 assert!(check(content).is_empty());
1023 }
1024
1025 #[test]
1028 fn fix_multiple_items() {
1029 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
1030 let fixed = fix(content);
1031 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
1032 }
1033
1034 #[test]
1035 fn fix_multiline_loose_continuation_all_lines() {
1036 let content = "1. Item\n\n line one\n line two\n line three\n";
1037 let fixed = fix(content);
1038 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
1039 }
1040
1041 #[test]
1044 fn sibling_item_boundary_respected() {
1045 let content = "- First\n- Second\n\n continuation\n";
1047 assert!(check(content).is_empty());
1048 }
1049
1050 #[test]
1053 fn blockquote_list_correct_indent_no_warning() {
1054 let content = "> - Item\n>\n> continuation\n";
1057 assert!(check(content).is_empty());
1058 }
1059
1060 #[test]
1061 fn blockquote_list_under_indent_no_false_positive() {
1062 let content = "> - Item\n>\n> continuation\n";
1067 assert!(check(content).is_empty());
1068 }
1069
1070 #[test]
1073 fn deeply_nested_correct_indent() {
1074 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1075 assert!(check(content).is_empty());
1076 }
1077
1078 #[test]
1079 fn deeply_nested_under_indent() {
1080 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1083 let warnings = check(content);
1084 assert_eq!(warnings.len(), 1);
1085 assert!(warnings[0].message.contains("6 spaces"));
1086 assert!(warnings[0].message.contains("found 5"));
1087 }
1088
1089 #[test]
1092 fn tab_indent_correct() {
1093 let content = "- Item\n\n\tcontinuation\n";
1096 assert!(check(content).is_empty());
1097 }
1098
1099 #[test]
1102 fn multiple_continuations_correct() {
1103 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
1104 assert!(check(content).is_empty());
1105 }
1106
1107 #[test]
1108 fn multiple_continuations_second_under_indent() {
1109 let content = "- Item\n\n para 1\n\n continuation 2\n";
1111 let warnings = check(content);
1112 assert_eq!(warnings.len(), 1);
1113 assert_eq!(warnings[0].line, 5);
1114 }
1115
1116 #[test]
1119 fn ordered_paren_marker_correct() {
1120 let content = "1) Item\n\n continuation\n";
1122 assert!(check(content).is_empty());
1123 }
1124
1125 #[test]
1126 fn ordered_paren_marker_under_indent() {
1127 let content = "1) Item\n\n continuation\n";
1128 let warnings = check(content);
1129 assert_eq!(warnings.len(), 1);
1130 assert!(warnings[0].message.contains("3 spaces"));
1131 }
1132
1133 #[test]
1136 fn star_marker_correct() {
1137 let content = "* Item\n\n continuation\n";
1138 assert!(check(content).is_empty());
1139 }
1140
1141 #[test]
1142 fn star_marker_under_indent() {
1143 let content = "* Item\n\n continuation\n";
1144 let warnings = check(content);
1145 assert_eq!(warnings.len(), 1);
1146 }
1147
1148 #[test]
1149 fn plus_marker_correct() {
1150 let content = "+ Item\n\n continuation\n";
1151 assert!(check(content).is_empty());
1152 }
1153
1154 #[test]
1157 fn heading_after_list_no_warning() {
1158 let content = "- Item\n\n# Heading\n";
1159 assert!(check(content).is_empty());
1160 }
1161
1162 #[test]
1165 fn hr_after_list_no_warning() {
1166 let content = "- Item\n\n---\n";
1167 assert!(check(content).is_empty());
1168 }
1169
1170 #[test]
1173 fn reference_link_def_not_flagged() {
1174 let content = "- Item\n\n [link]: https://example.com\n";
1175 assert!(check(content).is_empty());
1176 }
1177
1178 #[test]
1181 fn footnote_def_not_flagged() {
1182 let content = "- Item\n\n [^1]: footnote text\n";
1183 assert!(check(content).is_empty());
1184 }
1185
1186 #[test]
1189 fn fix_deeply_nested() {
1190 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
1191 let fixed = fix(content);
1192 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
1193 }
1194
1195 #[test]
1196 fn fix_mkdocs_unordered() {
1197 let content = "- Item\n\n continuation\n";
1199 let fixed = fix_mkdocs(content);
1200 assert_eq!(fixed, "- Item\n\n continuation\n");
1201 }
1202
1203 #[test]
1204 fn fix_code_fence_indent() {
1205 let content = "- Item\n\n ```\n code\n ```\n";
1208 let fixed = fix(content);
1209 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
1210 }
1211
1212 #[test]
1213 fn fix_mkdocs_code_fence_indent() {
1214 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
1216 let fixed = fix_mkdocs(content);
1217 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
1218 }
1219
1220 #[test]
1223 fn empty_document_no_warning() {
1224 assert!(check("").is_empty());
1225 }
1226
1227 #[test]
1228 fn whitespace_only_no_warning() {
1229 assert!(check(" \n\n \n").is_empty());
1230 }
1231
1232 #[test]
1235 fn no_list_no_warning() {
1236 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
1237 assert!(check(content).is_empty());
1238 }
1239
1240 #[test]
1243 fn multiline_continuation_all_lines_flagged() {
1244 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";
1245 let warnings = check(content);
1246 assert_eq!(warnings.len(), 3);
1247 assert_eq!(warnings[0].line, 3);
1248 assert_eq!(warnings[1].line, 4);
1249 assert_eq!(warnings[2].line, 5);
1250 }
1251
1252 #[test]
1253 fn multiline_continuation_with_frontmatter_fix() {
1254 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";
1255 let fixed = fix(content);
1256 assert_eq!(
1257 fixed,
1258 "---\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"
1259 );
1260 }
1261
1262 #[test]
1263 fn multiline_continuation_correct_indent_no_warning() {
1264 let content = "1. Item\n\n line one\n line two\n line three\n";
1265 assert!(check(content).is_empty());
1266 }
1267
1268 #[test]
1269 fn multiline_continuation_mixed_indent() {
1270 let content = "1. Item\n\n correct\n wrong\n correct\n";
1271 let warnings = check(content);
1272 assert_eq!(warnings.len(), 1);
1273 assert_eq!(warnings[0].line, 4);
1274 }
1275
1276 #[test]
1277 fn multiline_continuation_unordered() {
1278 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
1279 let warnings = check(content);
1280 assert_eq!(warnings.len(), 3);
1281 let fixed = fix(content);
1282 assert_eq!(
1283 fixed,
1284 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
1285 );
1286 }
1287
1288 #[test]
1289 fn multiline_continuation_two_items_fix() {
1290 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
1291 let fixed = fix(content);
1292 assert_eq!(
1293 fixed,
1294 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1295 );
1296 }
1297
1298 #[test]
1299 fn fence_fix_does_not_break_pairing_for_md031() {
1300 let content = "#### title\n\nabc\n\n\
1307 1. ab\n\n\
1308 \x20\x20`aabbccdd`\n\n\
1309 2. cd\n\n\
1310 \x20\x20`bbcc dd ee`\n\n\
1311 \x20\x20```\n\
1312 \x20\x20abcd\n\
1313 \x20\x20ef gh\n\
1314 \x20\x20```\n\n\
1315 \x20\x20uu\n\n\
1316 \x20\x20```\n\
1317 \x20\x20cdef\n\
1318 \x20\x20gh ij\n\
1319 \x20\x20```\n";
1320 let expected = "#### title\n\nabc\n\n\
1321 1. ab\n\n\
1322 \x20\x20\x20`aabbccdd`\n\n\
1323 2. cd\n\n\
1324 \x20\x20\x20`bbcc dd ee`\n\n\
1325 \x20\x20\x20```\n\
1326 \x20\x20\x20abcd\n\
1327 \x20\x20\x20ef gh\n\
1328 \x20\x20\x20```\n\n\
1329 \x20\x20\x20uu\n\n\
1330 \x20\x20\x20```\n\
1331 \x20\x20\x20cdef\n\
1332 \x20\x20\x20gh ij\n\
1333 \x20\x20\x20```\n";
1334 assert_eq!(fix(content), expected);
1335 }
1336
1337 #[test]
1338 fn multiline_continuation_separated_by_blank() {
1339 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1340 let warnings = check(content);
1341 assert_eq!(warnings.len(), 4);
1342 let fixed = fix(content);
1343 assert_eq!(
1344 fixed,
1345 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1346 );
1347 }
1348
1349 #[test]
1350 fn tab_indented_fence_is_normalized_to_spaces() {
1351 let content = "100. ab\n\n\t```\n\tabcd\n\t```\n";
1359 let expected = "100. ab\n\n ```\n abcd\n ```\n";
1360 assert_eq!(fix(content), expected);
1361 }
1362
1363 #[test]
1371 fn task_list_tight_continuation_post_checkbox_reproducer_579() {
1372 let content = "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n";
1375 assert!(check(content).is_empty());
1376 }
1377
1378 #[test]
1379 fn task_list_tight_continuation_dash_unchecked() {
1380 let content = "- [ ] Task\n continuation\n";
1381 assert!(check(content).is_empty());
1382 }
1383
1384 #[test]
1385 fn task_list_tight_continuation_dash_checked_lower() {
1386 let content = "- [x] Task\n continuation\n";
1387 assert!(check(content).is_empty());
1388 }
1389
1390 #[test]
1391 fn task_list_tight_continuation_dash_checked_upper() {
1392 let content = "- [X] Task\n continuation\n";
1393 assert!(check(content).is_empty());
1394 }
1395
1396 #[test]
1397 fn task_list_tight_continuation_star_marker() {
1398 let content = "* [ ] Task\n continuation\n";
1399 assert!(check(content).is_empty());
1400 }
1401
1402 #[test]
1403 fn task_list_tight_continuation_plus_marker() {
1404 let content = "+ [ ] Task\n continuation\n";
1405 assert!(check(content).is_empty());
1406 }
1407
1408 #[test]
1409 fn task_list_tight_continuation_content_column_still_valid() {
1410 let content = "- [ ] Task\n continuation\n";
1413 assert!(check(content).is_empty());
1414 }
1415
1416 #[test]
1417 fn task_list_tight_continuation_between_columns_still_flagged() {
1418 let content = "- [ ] Task\n continuation\n";
1421 let warnings = check(content);
1422 assert_eq!(warnings.len(), 1);
1423 assert!(warnings[0].message.contains("expected 2 or 6"));
1425 assert!(warnings[0].message.contains("found 4"));
1426 }
1427
1428 #[test]
1429 fn task_list_tight_continuation_overshoot_still_flagged() {
1430 let content = "- [ ] Task\n continuation\n";
1432 let warnings = check(content);
1433 assert_eq!(warnings.len(), 1);
1434 assert!(warnings[0].message.contains("expected 2 or 6"));
1435 assert!(warnings[0].message.contains("found 7"));
1436 }
1437
1438 #[test]
1441 fn fix_task_list_overshoot_snaps_to_task_col() {
1442 let content = "- [ ] Task\n continuation\n";
1446 let fixed = fix(content);
1447 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1448 }
1449
1450 #[test]
1451 fn fix_task_list_col_5_snaps_to_task_col() {
1452 let content = "- [ ] Task\n continuation\n";
1454 let fixed = fix(content);
1455 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1456 }
1457
1458 #[test]
1459 fn fix_task_list_col_3_snaps_to_content_col() {
1460 let content = "- [ ] Task\n continuation\n";
1462 let fixed = fix(content);
1463 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1464 }
1465
1466 #[test]
1467 fn fix_task_list_col_4_ties_to_content_col() {
1468 let content = "- [ ] Task\n continuation\n";
1473 let fixed = fix(content);
1474 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1475 }
1476
1477 #[test]
1478 fn fix_task_list_ordered_overshoot_snaps_to_task_col() {
1479 let content = "1. [ ] Task\n continuation\n";
1482 let fixed = fix(content);
1483 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1484 }
1485
1486 #[test]
1487 fn fix_task_list_ordered_under_overshoot_snaps_to_content_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 task_list_tight_continuation_ordered_single_digit() {
1497 let content = "1. [ ] Task\n continuation\n";
1499 assert!(check(content).is_empty());
1500 }
1501
1502 #[test]
1503 fn task_list_tight_continuation_ordered_multi_digit() {
1504 let content = "10. [ ] Task\n continuation\n";
1506 assert!(check(content).is_empty());
1507 }
1508
1509 #[test]
1510 fn task_list_tight_continuation_nested_dash() {
1511 let content = "- Parent\n - [ ] Nested task\n continuation\n";
1513 assert!(check(content).is_empty());
1514 }
1515
1516 #[test]
1517 fn task_list_loose_continuation_post_checkbox_column_not_flagged() {
1518 let content = "- [ ] Task\n\n continuation\n";
1523 assert!(check(content).is_empty());
1524 }
1525
1526 #[test]
1527 fn task_list_empty_body_is_not_a_task() {
1528 let content = "- [ ]\n continuation\n";
1534 let warnings = check(content);
1535 assert_eq!(warnings.len(), 1);
1536 assert!(warnings[0].message.contains("found 4"));
1537 }
1538
1539 #[test]
1540 fn task_list_malformed_checkbox_is_not_a_task() {
1541 let content = "- [~] Not a task\n continuation\n";
1543 let warnings = check(content);
1544 assert_eq!(warnings.len(), 1);
1545 }
1546
1547 #[test]
1554 fn task_list_mkdocs_unordered_required_min_valid() {
1555 let content = "- [ ] Task\n continuation\n";
1557 assert!(check_mkdocs(content).is_empty());
1558 }
1559
1560 #[test]
1561 fn task_list_mkdocs_unordered_post_checkbox_valid() {
1562 let content = "- [ ] Task\n continuation\n";
1563 assert!(check_mkdocs(content).is_empty());
1564 }
1565
1566 #[test]
1567 fn task_list_mkdocs_unordered_between_flagged() {
1568 let content = "- [ ] Task\n continuation\n";
1570 let warnings = check_mkdocs(content);
1571 assert_eq!(warnings.len(), 1);
1572 }
1573
1574 #[test]
1575 fn task_list_mkdocs_ordered_both_columns_valid() {
1576 let at_4 = "1. [ ] Task\n continuation\n";
1578 assert!(check_mkdocs(at_4).is_empty());
1579 let at_7 = "1. [ ] Task\n continuation\n";
1580 assert!(check_mkdocs(at_7).is_empty());
1581 }
1582
1583 #[test]
1584 fn task_list_mkdocs_ordered_between_flagged() {
1585 let at_5 = "1. [ ] Task\n continuation\n";
1587 assert_eq!(check_mkdocs(at_5).len(), 1);
1588 let at_6 = "1. [ ] Task\n continuation\n";
1589 assert_eq!(check_mkdocs(at_6).len(), 1);
1590 }
1591
1592 #[test]
1602 fn fix_task_list_tie_sibling_at_task_col_snaps_to_task_col() {
1603 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
1607 let fixed = fix(content);
1608 assert_eq!(
1609 fixed,
1610 "- [ ] Task\n aligned continuation\n tied continuation\n"
1611 );
1612 }
1613
1614 #[test]
1615 fn fix_task_list_tie_sibling_at_content_col_snaps_to_content_col() {
1616 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
1619 let fixed = fix(content);
1620 assert_eq!(fixed, "- [ ] Task\n aligned continuation\n tied continuation\n");
1621 }
1622
1623 #[test]
1624 fn fix_task_list_tie_both_siblings_snaps_to_content_col() {
1625 let content = "- [ ] Task\n at content col\n at task col\n tied continuation\n";
1629 let fixed = fix(content);
1630 assert_eq!(
1631 fixed,
1632 "- [ ] Task\n at content col\n at task col\n tied continuation\n"
1633 );
1634 }
1635
1636 #[test]
1637 fn fix_task_list_tie_sees_task_col_through_tight_lazy_continuation() {
1638 let content = concat!("- [ ] Task\n", "lazy\n", " aligned at task col\n", " tied\n",);
1652 let fixed = fix(content);
1653 assert!(
1654 fixed.contains("\n tied\n"),
1655 "tied line should snap to col 6 (task col) because a task-col \
1656 sibling is visible past the tight lazy-continuation line; got:\n{fixed}"
1657 );
1658 }
1659
1660 #[test]
1667 fn task_list_tab_indented_continuation_flagged() {
1668 let content = "- [ ] Task\n\t\twrap\n";
1671 let warnings = check(content);
1672 assert_eq!(warnings.len(), 1);
1673 assert!(warnings[0].message.contains("expected 2 or 6"));
1674 assert!(warnings[0].message.contains("found 8"));
1675 }
1676
1677 #[test]
1678 fn fix_task_list_tab_indented_snaps_to_task_col() {
1679 let content = "- [ ] Task\n\t\twrap\n";
1681 let fixed = fix(content);
1682 assert_eq!(fixed, "- [ ] Task\n wrap\n");
1683 }
1684
1685 #[test]
1686 fn fix_task_list_single_tab_equidistant_snaps_to_content_col() {
1687 let content = "- [ ] Task\n\twrap\n";
1690 let fixed = fix(content);
1691 assert_eq!(fixed, "- [ ] Task\n wrap\n");
1692 }
1693
1694 #[test]
1704 fn task_list_blockquote_post_checkbox_not_flagged() {
1705 let content = "> - [ ] Task\n> continuation\n";
1707 assert!(check(content).is_empty());
1708 }
1709
1710 #[test]
1711 fn task_list_blockquote_between_cols_documented_limitation() {
1712 let content = "> - [ ] Task\n> continuation\n";
1716 assert!(check(content).is_empty());
1717 }
1718
1719 #[test]
1720 fn task_list_blockquote_overshoot_documented_limitation() {
1721 let content = "> - [ ] Task\n> continuation\n";
1723 assert!(check(content).is_empty());
1724 }
1725
1726 #[test]
1733 fn fix_task_list_mkdocs_unordered_overshoot_snaps_to_task_col() {
1734 let content = "- [ ] Task\n continuation\n";
1737 let fixed = fix_mkdocs(content);
1738 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1739 }
1740
1741 #[test]
1742 fn fix_task_list_mkdocs_unordered_tie_snaps_to_required() {
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_ordered_overshoot_snaps_to_task_col() {
1752 let content = "1. [ ] Task\n continuation\n";
1755 let fixed = fix_mkdocs(content);
1756 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1757 }
1758
1759 #[test]
1760 fn fix_task_list_mkdocs_ordered_near_required_snaps_to_required() {
1761 let content = "1. [ ] Task\n continuation\n";
1767 let fixed = fix_mkdocs(content);
1768 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1769 }
1770
1771 #[test]
1772 fn fix_task_list_mkdocs_ordered_between_cols_snaps_to_task_col() {
1773 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 fn assert_idempotent(content: &str) {
1790 let once = fix(content);
1791 let twice = fix(&once);
1792 assert_eq!(once, twice, "MD077 fix was not idempotent on input: {content:?}");
1793 }
1794
1795 fn assert_idempotent_mkdocs(content: &str) {
1796 let once = fix_mkdocs(content);
1797 let twice = fix_mkdocs(&once);
1798 assert_eq!(
1799 once, twice,
1800 "MD077 (MkDocs) fix was not idempotent on input: {content:?}"
1801 );
1802 }
1803
1804 #[test]
1805 fn idempotent_task_list_between_cols() {
1806 assert_idempotent("- [ ] Task\n continuation\n");
1807 }
1808
1809 #[test]
1810 fn idempotent_task_list_overshoot() {
1811 assert_idempotent("- [ ] Task\n continuation\n");
1812 }
1813
1814 #[test]
1815 fn idempotent_task_list_under_post_checkbox() {
1816 assert_idempotent("- [ ] Task\n continuation\n");
1817 }
1818
1819 #[test]
1820 fn idempotent_task_list_near_post_checkbox() {
1821 assert_idempotent("- [ ] Task\n continuation\n");
1822 }
1823
1824 #[test]
1825 fn idempotent_task_list_tab_overshoot() {
1826 assert_idempotent("- [ ] Task\n\t\twrap\n");
1827 }
1828
1829 #[test]
1830 fn idempotent_task_list_single_tab() {
1831 assert_idempotent("- [ ] Task\n\twrap\n");
1832 }
1833
1834 #[test]
1835 fn idempotent_task_list_ordered_overshoot() {
1836 assert_idempotent("1. [ ] Task\n continuation\n");
1837 }
1838
1839 #[test]
1840 fn idempotent_task_list_ordered_under() {
1841 assert_idempotent("1. [ ] Task\n continuation\n");
1842 }
1843
1844 #[test]
1845 fn idempotent_task_list_tie_with_sibling_at_task_col() {
1846 assert_idempotent("- [ ] Task\n aligned\n tied\n");
1847 }
1848
1849 #[test]
1850 fn idempotent_task_list_tie_with_sibling_at_content_col() {
1851 assert_idempotent("- [ ] Task\n aligned\n tied\n");
1852 }
1853
1854 #[test]
1855 fn idempotent_task_list_mkdocs_unordered_overshoot() {
1856 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
1857 }
1858
1859 #[test]
1860 fn idempotent_task_list_mkdocs_unordered_tie() {
1861 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
1862 }
1863
1864 #[test]
1865 fn idempotent_task_list_mkdocs_ordered_overshoot() {
1866 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
1867 }
1868
1869 #[test]
1870 fn idempotent_task_list_mkdocs_ordered_between() {
1871 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
1872 }
1873
1874 #[test]
1875 fn idempotent_task_list_reproducer_579() {
1876 assert_idempotent(
1880 "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n",
1881 );
1882 }
1883
1884 #[test]
1885 fn idempotent_non_task_list_still_holds() {
1886 assert_idempotent("1. Item\n over-indented\n");
1889 assert_idempotent("- Item\n\n continuation\n");
1890 }
1891
1892 #[test]
1899 fn idempotent_non_task_loose_under_indent_ordered() {
1900 assert_idempotent("1. Item\n\n continuation\n");
1902 }
1903
1904 #[test]
1905 fn idempotent_non_task_loose_under_indent_multi_digit() {
1906 assert_idempotent("10. Item\n\n continuation\n");
1908 }
1909
1910 #[test]
1911 fn idempotent_non_task_tight_over_indent_ordered() {
1912 assert_idempotent("1. Item\n over-indented\n");
1914 }
1915
1916 #[test]
1924 fn idempotent_non_task_fence_ordered_loose() {
1925 assert_idempotent("1. Item\n\n ```rust\n let x = 1;\n ```\n");
1927 }
1928
1929 #[test]
1930 fn idempotent_non_task_fence_tilde_under_indent() {
1931 assert_idempotent("1. Item\n\n ~~~\nplain text\n ~~~\n");
1937 }
1938
1939 #[test]
1940 fn idempotent_non_task_fence_interior_above_required() {
1941 assert_idempotent("1. Item\n\n ```\n deeply indented code\n ```\n");
1945 }
1946
1947 #[test]
1948 fn fence_fix_promotes_interior_below_scope_in_single_pass() {
1949 let content = "1. Item\n\n ```\ncode\n ```\n";
1953 let fixed = fix(content);
1954 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
1955 }
1956
1957 #[test]
1958 fn fence_fix_preserves_interior_above_required() {
1959 let content = "1. Item\n\n ```\n code\n ```\n";
1962 let fixed = fix(content);
1963 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
1964 }
1965
1966 #[test]
1973 fn idempotent_non_task_mkdocs_ordered_at_3_spaces() {
1974 assert_idempotent_mkdocs("1. Item\n\n continuation\n");
1976 }
1977
1978 #[test]
1979 fn idempotent_non_task_mkdocs_unordered_at_2_spaces() {
1980 assert_idempotent_mkdocs("- Item\n\n continuation\n");
1982 }
1983
1984 #[test]
1985 fn idempotent_non_task_mkdocs_fence_compound() {
1986 assert_idempotent_mkdocs("1. Item\n\n ```toml\n k = 1\n ```\n");
1988 }
1989}