1use std::ops::ControlFlow;
7
8use serde::{Deserialize, Serialize};
9
10use crate::lint_context::{LineInfo, LintContext};
11use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
12
13mod md077_config;
14use md077_config::MD077Config;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
18#[serde(rename_all = "kebab-case")]
19pub enum ContinuationStyle {
20 #[default]
24 Any,
25 Aligned,
29}
30
31#[derive(Clone, Default)]
47pub struct MD077ListContinuationIndent {
48 config: MD077Config,
49}
50
51impl MD077ListContinuationIndent {
52 pub fn new(style: ContinuationStyle) -> Self {
55 Self {
56 config: MD077Config { style },
57 }
58 }
59
60 pub fn from_config_struct(config: MD077Config) -> Self {
61 Self { config }
62 }
63}
64
65impl MD077ListContinuationIndent {
66 const TASK_CHECKBOX_PREFIX_LEN: usize = 4;
69
70 fn is_task_list_item(line: &str, content_col: usize) -> bool {
86 line.as_bytes()
87 .get(content_col..content_col + Self::TASK_CHECKBOX_PREFIX_LEN)
88 .is_some_and(|window| matches!(window, b"[ ] " | b"[x] " | b"[X] "))
89 }
90
91 fn is_block_level_construct(trimmed: &str) -> bool {
93 if trimmed.starts_with("[^") && trimmed.contains("]:") {
95 return true;
96 }
97 if trimmed.starts_with("*[") && trimmed.contains("]:") {
99 return true;
100 }
101 if trimmed.starts_with('[') && !trimmed.starts_with("[^") && trimmed.contains("]: ") {
104 return true;
105 }
106 false
107 }
108
109 fn is_code_fence(trimmed: &str) -> bool {
111 let bytes = trimmed.as_bytes();
112 if bytes.len() < 3 {
113 return false;
114 }
115 let ch = bytes[0];
116 (ch == b'`' || ch == b'~') && bytes[1] == ch && bytes[2] == ch
117 }
118
119 fn starts_with_list_marker(trimmed: &str) -> bool {
123 let bytes = trimmed.as_bytes();
124 match bytes.first() {
125 Some(b'*' | b'-' | b'+') => bytes.get(1).is_some_and(|&b| b == b' ' || b == b'\t'),
126 Some(b'0'..=b'9') => {
127 let rest = trimmed.trim_start_matches(|c: char| c.is_ascii_digit());
128 rest.starts_with(". ") || rest.starts_with(") ")
129 }
130 _ => false,
131 }
132 }
133
134 fn find_fence_closer(ctx: &LintContext, opener_line: usize) -> usize {
138 let mut closer_line = opener_line;
139 for peek in (opener_line + 1)..=ctx.lines.len() {
140 let Some(peek_info) = ctx.line_info(peek) else { break };
141 if peek_info.in_code_block {
142 closer_line = peek;
143 } else {
144 break;
145 }
146 }
147 closer_line
148 }
149
150 fn build_compound_fence_fix(
185 ctx: &LintContext,
186 opener_line: usize,
187 closer_line: usize,
188 opener_actual: usize,
189 required: usize,
190 ) -> Option<Fix> {
191 if required <= opener_actual {
192 return None;
193 }
194 let opener_info = ctx.line_info(opener_line)?;
195 let closer_info = ctx.line_info(closer_line)?;
196
197 let fix_start = opener_info.byte_offset;
198 let fix_end = closer_info.byte_offset + closer_info.byte_len;
199
200 let mut replacement = String::new();
201 for i in opener_line..=closer_line {
202 let info = ctx.line_info(i)?;
203 if i > opener_line {
204 replacement.push('\n');
205 }
206 let line = info.content(ctx.content);
207 if info.is_blank {
208 replacement.push_str(line);
210 } else {
211 let new_visual = if i == opener_line || i == closer_line {
212 required
213 } else {
214 info.visual_indent.max(required)
215 };
216 for _ in 0..new_visual {
217 replacement.push(' ');
218 }
219 replacement.push_str(&line[info.indent..]);
220 }
221 }
222
223 Some(Fix::new(fix_start..fix_end, replacement))
224 }
225
226 fn walk_item_continuation<F>(
249 ctx: &LintContext,
250 item_line: usize,
251 range_end: usize,
252 marker_col: usize,
253 mut per_line: F,
254 ) where
255 F: FnMut(&ContinuationLine<'_>) -> ControlFlow<()>,
256 {
257 let mut saw_blank = false;
258 let mut saw_nested = false;
259 let mut nested_content_col: Option<usize> = None;
260
261 for line_num in (item_line + 1)..=range_end {
262 let Some(info) = ctx.line_info(line_num) else {
263 continue;
264 };
265
266 let trimmed = info.content(ctx.content).trim_start();
267
268 if Self::should_skip_line(info, trimmed) {
269 continue;
270 }
271
272 if info.is_blank {
273 saw_blank = true;
274 continue;
275 }
276
277 if let Some(ref li) = info.list_item {
278 if li.marker_column > marker_col {
279 nested_content_col = Some(li.content_column);
280 saw_nested = true;
285 } else {
286 nested_content_col = None;
287 }
288 saw_blank = false;
289 continue;
290 }
291
292 if info.heading.is_some() || info.is_horizontal_rule {
293 break;
294 }
295
296 if Self::is_block_level_construct(trimmed) {
297 continue;
298 }
299
300 let col = info.visual_indent;
301
302 if let Some(ncc) = nested_content_col {
303 if col >= ncc {
304 continue;
305 }
306 nested_content_col = None;
307 }
308
309 if saw_blank && col <= marker_col {
310 break;
311 }
312
313 let line = ContinuationLine {
314 line_num,
315 info,
316 trimmed,
317 actual: col,
318 saw_blank,
319 saw_nested,
320 };
321 if per_line(&line).is_break() {
322 break;
323 }
324 }
325 }
326
327 fn sibling_column_usage(
337 ctx: &LintContext,
338 item_line: usize,
339 range_end: usize,
340 marker_col: usize,
341 content_col: usize,
342 task_col: usize,
343 ) -> (bool, bool) {
344 let mut uses_content = false;
345 let mut uses_task = false;
346
347 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
348 if line.actual == content_col {
349 uses_content = true;
350 }
351 if line.actual == task_col {
352 uses_task = true;
353 }
354 if uses_content && uses_task {
355 ControlFlow::Break(())
356 } else {
357 ControlFlow::Continue(())
358 }
359 });
360
361 (uses_content, uses_task)
362 }
363
364 fn compute_fix_target(
370 actual: usize,
371 required: usize,
372 task_col: Option<usize>,
373 uses_content_col: bool,
374 uses_task_col: bool,
375 ) -> usize {
376 let Some(t) = task_col else { return required };
377 match actual.abs_diff(t).cmp(&actual.abs_diff(required)) {
378 std::cmp::Ordering::Less => t,
379 std::cmp::Ordering::Greater => required,
380 std::cmp::Ordering::Equal => match (uses_task_col, uses_content_col) {
381 (true, false) => t,
382 _ => required,
383 },
384 }
385 }
386
387 fn should_skip_line(info: &crate::lint_context::LineInfo, trimmed: &str) -> bool {
398 if info.in_code_block && !Self::is_code_fence(trimmed) {
399 return true;
400 }
401 info.in_front_matter
402 || info.in_footnote_definition
403 || info.in_html_block
404 || info.in_html_comment
405 || info.in_mdx_comment
406 || info.in_mkdocstrings
407 || info.in_esm_block
408 || info.in_math_block
409 || info.in_admonition
410 || info.in_content_tab
411 || info.in_pymdown_block
412 || info.in_definition_list
413 || info.in_mkdocs_html_markdown
414 || info.in_kramdown_extension_block
415 }
416
417 fn build_over_indent_warning(
426 ctx: &LintContext,
427 line: &ContinuationLine<'_>,
428 fix_target: usize,
429 message: String,
430 ) -> LintWarning {
431 let line_content = line.info.content(ctx.content);
432 let fix_start = line.info.byte_offset;
433 let fix_end = fix_start + line.info.indent;
434 LintWarning {
435 rule_name: Some("MD077".to_string()),
436 line: line.line_num,
437 column: 1,
438 end_line: line.line_num,
439 end_column: line_content.chars().count() + 1,
440 message,
441 severity: Severity::Warning,
442 fix: Some(Fix::new(fix_start..fix_end, " ".repeat(fix_target))),
443 }
444 }
445
446 fn build_under_indent_warning(
458 ctx: &LintContext,
459 line: &ContinuationLine<'_>,
460 required: usize,
461 message: String,
462 ) -> UnderIndentOutcome {
463 let line_content = line.info.content(ctx.content);
464 let is_fence_opener = line.info.in_code_block
465 && Self::is_code_fence(line.trimmed)
466 && ctx.line_info(line.line_num - 1).is_none_or(|p| !p.in_code_block);
467
468 let (fix, warn_end_line, warn_end_column, compound_closer) = if is_fence_opener {
469 let closer_line = Self::find_fence_closer(ctx, line.line_num);
470 let fix = Self::build_compound_fence_fix(ctx, line.line_num, closer_line, line.actual, required);
471 let end_column = ctx
472 .line_info(closer_line)
473 .map_or(line_content.chars().count() + 1, |ci| {
474 ci.content(ctx.content).chars().count() + 1
475 });
476 let extra_flag = (closer_line != line.line_num).then_some(closer_line);
477 (fix, closer_line, end_column, extra_flag)
478 } else {
479 let fix_start = line.info.byte_offset;
480 let fix_end = fix_start + line.info.indent;
481 let fix = Some(Fix::new(fix_start..fix_end, " ".repeat(required)));
482 (fix, line.line_num, line_content.chars().count() + 1, None)
483 };
484
485 UnderIndentOutcome {
486 warning: LintWarning {
487 rule_name: Some("MD077".to_string()),
488 line: line.line_num,
489 column: 1,
490 end_line: warn_end_line,
491 end_column: warn_end_column,
492 message,
493 severity: Severity::Warning,
494 fix,
495 },
496 also_flag_line: compound_closer,
497 }
498 }
499}
500
501struct ContinuationLine<'a> {
505 line_num: usize,
506 info: &'a LineInfo,
507 trimmed: &'a str,
508 actual: usize,
509 saw_blank: bool,
510 saw_nested: bool,
514}
515
516struct UnderIndentOutcome {
521 warning: LintWarning,
522 also_flag_line: Option<usize>,
523}
524
525impl Rule for MD077ListContinuationIndent {
526 fn name(&self) -> &'static str {
527 "MD077"
528 }
529
530 fn description(&self) -> &'static str {
531 "List continuation content indentation"
532 }
533
534 fn check(&self, ctx: &LintContext) -> LintResult {
535 if ctx.content.is_empty() {
536 return Ok(Vec::new());
537 }
538
539 let strict_indent = ctx.flavor.requires_strict_list_indent();
540 let total_lines = ctx.lines.len();
541 let mut warnings = Vec::new();
542 let mut flagged_lines = std::collections::HashSet::new();
543
544 let mut items: Vec<(usize, usize, usize, Option<usize>)> = Vec::new();
553 for block in &ctx.list_blocks {
554 for &item_line in &block.item_lines {
555 if let Some(info) = ctx.line_info(item_line)
556 && let Some(ref li) = info.list_item
557 {
558 let line = info.content(ctx.content);
559 let task_col = Self::is_task_list_item(line, li.content_column)
560 .then_some(li.content_column + Self::TASK_CHECKBOX_PREFIX_LEN);
561 items.push((item_line, li.marker_column, li.content_column, task_col));
562 }
563 }
564 }
565 items.sort_unstable();
566 items.dedup_by_key(|&mut (ln, _, _, _)| ln);
567
568 let scoped: Vec<(usize, usize, usize, Option<usize>, usize, usize)> = items
572 .iter()
573 .enumerate()
574 .map(|(item_idx, &(item_line, marker_col, content_col, task_col))| {
575 let required = if strict_indent { content_col.max(4) } else { content_col };
576 let range_end = items
577 .iter()
578 .skip(item_idx + 1)
579 .find(|&&(_, mc, _, _)| mc <= marker_col)
580 .map_or(total_lines, |&(ln, _, _, _)| ln - 1);
581 (item_line, marker_col, content_col, task_col, required, range_end)
582 })
583 .collect();
584
585 let aligned = self.config.style == ContinuationStyle::Aligned;
611 for &(item_line, marker_col, _content_col, _task_col, required, range_end) in &scoped {
612 let has_latent_structure = aligned && {
627 let mut found = false;
628 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
629 if Self::starts_with_list_marker(line.trimmed)
630 || crate::utils::skip_context::is_table_line(line.trimmed)
631 {
632 found = true;
633 ControlFlow::Break(())
634 } else {
635 ControlFlow::Continue(())
636 }
637 });
638 found
639 };
640 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
641 let actual = line.actual;
642 let under_indented = actual < required;
643 let loose_escape = line.saw_blank && under_indented;
644 let confirmed_structure = line.info.in_code_block || line.info.blockquote.is_some();
651 let aligned_tight = aligned
652 && !has_latent_structure
653 && !line.saw_blank
654 && !line.saw_nested
655 && under_indented
656 && !confirmed_structure;
657 if (loose_escape || aligned_tight) && flagged_lines.insert(line.line_num) {
658 let message = if line.saw_blank {
659 if strict_indent {
660 format!(
661 "Content inside list item needs {required} spaces of indentation \
662 for MkDocs compatibility (found {actual})",
663 )
664 } else {
665 format!(
666 "Content after blank line in list item needs {required} spaces of \
667 indentation to remain part of the list (found {actual})",
668 )
669 }
670 } else {
671 format!("Continuation line under-indented (expected {required}, found {actual})")
672 };
673 let outcome = Self::build_under_indent_warning(ctx, line, required, message);
674 if let Some(closer_line) = outcome.also_flag_line {
675 flagged_lines.insert(closer_line);
676 }
677 warnings.push(outcome.warning);
678 }
679 ControlFlow::Continue(())
680 });
681 }
682
683 for &(item_line, marker_col, content_col, task_col, required, range_end) in &scoped {
692 let (uses_content_col, uses_task_col) = match task_col {
696 Some(t) => Self::sibling_column_usage(ctx, item_line, range_end, marker_col, content_col, t),
697 None => (false, false),
698 };
699
700 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
701 let actual = line.actual;
702 if actual > required
703 && !line.info.in_code_block
704 && Some(actual) != task_col
705 && !Self::starts_with_list_marker(line.trimmed)
706 && flagged_lines.insert(line.line_num)
707 {
708 let fix_target =
709 Self::compute_fix_target(actual, required, task_col, uses_content_col, uses_task_col);
710 let message = match task_col {
711 Some(t) => format!(
712 "Continuation line over-indented \
713 (expected {required} or {t}, found {actual})"
714 ),
715 None => {
716 format!("Continuation line over-indented (expected {required}, found {actual})")
717 }
718 };
719 warnings.push(Self::build_over_indent_warning(ctx, line, fix_target, message));
720 }
721 ControlFlow::Continue(())
722 });
723 }
724
725 warnings.sort_by_key(|w| (w.line, w.column));
728
729 Ok(warnings)
730 }
731
732 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
733 let warnings = self.check(ctx)?;
734 let warnings =
735 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
736 if warnings.is_empty() {
737 return Ok(ctx.content.to_string());
738 }
739
740 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
742 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
743
744 let mut content = ctx.content.to_string();
745 for fix in fixes {
746 if fix.range.start <= content.len() && fix.range.end <= content.len() {
747 content.replace_range(fix.range, &fix.replacement);
748 }
749 }
750
751 Ok(content)
752 }
753
754 fn category(&self) -> RuleCategory {
755 RuleCategory::List
756 }
757
758 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
759 ctx.content.is_empty() || ctx.list_blocks.is_empty()
760 }
761
762 fn as_any(&self) -> &dyn std::any::Any {
763 self
764 }
765
766 crate::impl_rule_config_methods!(MD077Config);
767}
768
769#[cfg(test)]
770mod tests {
771 use super::*;
772 use crate::config::MarkdownFlavor;
773
774 fn check(content: &str) -> Vec<LintWarning> {
775 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
776 let rule = MD077ListContinuationIndent::default();
777 rule.check(&ctx).unwrap()
778 }
779
780 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
781 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
782 let rule = MD077ListContinuationIndent::default();
783 rule.check(&ctx).unwrap()
784 }
785
786 fn fix(content: &str) -> String {
787 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
788 let rule = MD077ListContinuationIndent::default();
789 rule.fix(&ctx).unwrap()
790 }
791
792 fn fix_mkdocs(content: &str) -> String {
793 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
794 let rule = MD077ListContinuationIndent::default();
795 rule.fix(&ctx).unwrap()
796 }
797
798 fn aligned_rule() -> MD077ListContinuationIndent {
799 MD077ListContinuationIndent::new(ContinuationStyle::Aligned)
800 }
801
802 fn check_aligned(content: &str) -> Vec<LintWarning> {
803 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
804 aligned_rule().check(&ctx).unwrap()
805 }
806
807 fn check_aligned_mkdocs(content: &str) -> Vec<LintWarning> {
808 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
809 aligned_rule().check(&ctx).unwrap()
810 }
811
812 fn fix_aligned(content: &str) -> String {
813 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
814 aligned_rule().fix(&ctx).unwrap()
815 }
816
817 #[test]
820 fn tight_lazy_continuation_zero_indent_not_flagged() {
821 let content = "- Item\ncontinuation\n";
823 assert!(check(content).is_empty());
824 }
825
826 #[test]
827 fn tight_continuation_correct_indent_not_flagged() {
828 let content = "1. Item\n continuation\n";
830 assert!(check(content).is_empty());
831 }
832
833 #[test]
834 fn tight_continuation_over_indented_ordered() {
835 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
837 let warnings = check(content);
838 assert_eq!(warnings.len(), 1);
839 assert_eq!(warnings[0].line, 2);
840 assert!(warnings[0].message.contains("over-indented"));
841 }
842
843 #[test]
844 fn tight_continuation_over_indented_unordered() {
845 let content = "- Item\n over-indented\n";
847 let warnings = check(content);
848 assert_eq!(warnings.len(), 1);
849 assert_eq!(warnings[0].line, 2);
850 }
851
852 #[test]
853 fn tight_continuation_multiple_over_indented_lines() {
854 let content = "1. Item\n line one\n line two\n line three\n";
855 let warnings = check(content);
856 assert_eq!(warnings.len(), 3);
857 }
858
859 #[test]
860 fn tight_continuation_mixed_correct_and_over() {
861 let content = "1. Item\n correct\n over-indented\n correct again\n";
862 let warnings = check(content);
863 assert_eq!(warnings.len(), 1);
864 assert_eq!(warnings[0].line, 3);
865 }
866
867 #[test]
868 fn tight_continuation_nested_over_indented() {
869 let content = "- L1\n - L2\n over-indented continuation of L2\n";
871 let warnings = check(content);
872 assert_eq!(warnings.len(), 1);
873 assert_eq!(warnings[0].line, 3);
874 assert!(warnings[0].message.contains("expected 4"));
876 assert!(warnings[0].message.contains("found 5"));
877 }
878
879 #[test]
880 fn tight_continuation_nested_correct_indent_not_flagged() {
881 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
884 assert!(check(content).is_empty());
885 }
886
887 #[test]
888 fn fix_tight_continuation_nested_over_indented() {
889 let content = "- L1\n - L2\n over-indented continuation of L2\n";
891 let fixed = fix(content);
892 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
893 }
894
895 #[test]
896 fn tight_continuation_under_indented_not_flagged() {
897 let content = "1. Item\n under-indented\n";
900 assert!(check(content).is_empty());
901 }
902
903 #[test]
904 fn tight_continuation_tab_over_indented() {
905 let content = "- Item\n\tover-indented\n";
907 let warnings = check(content);
908 assert_eq!(warnings.len(), 1);
909 }
910
911 #[test]
912 fn fix_tight_continuation_over_indented_ordered() {
913 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
914 let fixed = fix(content);
915 assert_eq!(
916 fixed,
917 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
918 );
919 }
920
921 #[test]
922 fn fix_tight_continuation_over_indented_unordered() {
923 let content = "- Item\n over-indented\n";
924 let fixed = fix(content);
925 assert_eq!(fixed, "- Item\n over-indented\n");
926 }
927
928 #[test]
929 fn fix_tight_continuation_multiple_lines() {
930 let content = "1. Item\n line one\n line two\n";
931 let fixed = fix(content);
932 assert_eq!(fixed, "1. Item\n line one\n line two\n");
933 }
934
935 #[test]
936 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
937 let content = "1. Item\n continuation\n";
940 assert!(check_mkdocs(content).is_empty());
941 }
942
943 #[test]
944 fn tight_continuation_mkdocs_5space_ordered_flagged() {
945 let content = "1. Item\n over-indented\n";
947 let warnings = check_mkdocs(content);
948 assert_eq!(warnings.len(), 1);
949 assert!(warnings[0].message.contains("expected 4"));
950 assert!(warnings[0].message.contains("found 5"));
951 }
952
953 #[test]
954 fn fix_tight_continuation_mkdocs_over_indented() {
955 let content = "1. Item\n over-indented\n";
956 let fixed = fix_mkdocs(content);
957 assert_eq!(fixed, "1. Item\n over-indented\n");
958 }
959
960 #[test]
961 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
962 let content = "* Level 0\n * Level 1\n * Level 2\n";
965 assert!(check(content).is_empty());
966 }
967
968 #[test]
969 fn tight_continuation_ordered_marker_not_flagged() {
970 let content = "- Parent\n 1. Child item\n";
972 assert!(check(content).is_empty());
973 }
974
975 #[test]
978 fn unordered_correct_indent_no_warning() {
979 let content = "- Item\n\n continuation\n";
980 assert!(check(content).is_empty());
981 }
982
983 #[test]
984 fn unordered_partial_indent_warns() {
985 let content = "- Item\n\n continuation\n";
988 let warnings = check(content);
989 assert_eq!(warnings.len(), 1);
990 assert_eq!(warnings[0].line, 3);
991 assert!(warnings[0].message.contains("2 spaces"));
992 assert!(warnings[0].message.contains("found 1"));
993 }
994
995 #[test]
996 fn unordered_zero_indent_is_new_paragraph() {
997 let content = "- Item\n\ncontinuation\n";
1000 assert!(check(content).is_empty());
1001 }
1002
1003 #[test]
1006 fn ordered_3space_correct_commonmark() {
1007 let content = "1. Item\n\n continuation\n";
1009 assert!(check(content).is_empty());
1010 }
1011
1012 #[test]
1013 fn ordered_2space_under_indent_commonmark() {
1014 let content = "1. Item\n\n continuation\n";
1015 let warnings = check(content);
1016 assert_eq!(warnings.len(), 1);
1017 assert!(warnings[0].message.contains("3 spaces"));
1018 assert!(warnings[0].message.contains("found 2"));
1019 }
1020
1021 #[test]
1024 fn multi_digit_marker_correct() {
1025 let content = "10. Item\n\n continuation\n";
1027 assert!(check(content).is_empty());
1028 }
1029
1030 #[test]
1031 fn multi_digit_marker_under_indent() {
1032 let content = "10. Item\n\n continuation\n";
1033 let warnings = check(content);
1034 assert_eq!(warnings.len(), 1);
1035 assert!(warnings[0].message.contains("4 spaces"));
1036 }
1037
1038 #[test]
1041 fn mkdocs_3space_ordered_warns() {
1042 let content = "1. Item\n\n continuation\n";
1044 let warnings = check_mkdocs(content);
1045 assert_eq!(warnings.len(), 1);
1046 assert!(warnings[0].message.contains("4 spaces"));
1047 assert!(warnings[0].message.contains("MkDocs"));
1048 }
1049
1050 #[test]
1051 fn mkdocs_4space_ordered_no_warning() {
1052 let content = "1. Item\n\n continuation\n";
1053 assert!(check_mkdocs(content).is_empty());
1054 }
1055
1056 #[test]
1057 fn mkdocs_unordered_2space_ok() {
1058 let content = "- Item\n\n continuation\n";
1060 assert!(check_mkdocs(content).is_empty());
1061 }
1062
1063 #[test]
1064 fn mkdocs_unordered_2space_warns() {
1065 let content = "- Item\n\n continuation\n";
1067 let warnings = check_mkdocs(content);
1068 assert_eq!(warnings.len(), 1);
1069 assert!(warnings[0].message.contains("4 spaces"));
1070 }
1071
1072 #[test]
1075 fn fix_unordered_indent() {
1076 let content = "- Item\n\n continuation\n";
1078 let fixed = fix(content);
1079 assert_eq!(fixed, "- Item\n\n continuation\n");
1080 }
1081
1082 #[test]
1083 fn fix_ordered_indent() {
1084 let content = "1. Item\n\n continuation\n";
1085 let fixed = fix(content);
1086 assert_eq!(fixed, "1. Item\n\n continuation\n");
1087 }
1088
1089 #[test]
1090 fn fix_mkdocs_indent() {
1091 let content = "1. Item\n\n continuation\n";
1092 let fixed = fix_mkdocs(content);
1093 assert_eq!(fixed, "1. Item\n\n continuation\n");
1094 }
1095
1096 #[test]
1099 fn nested_list_items_not_flagged() {
1100 let content = "- Parent\n\n - Child\n";
1101 assert!(check(content).is_empty());
1102 }
1103
1104 #[test]
1105 fn nested_list_zero_indent_is_new_paragraph() {
1106 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
1108 assert!(check(content).is_empty());
1109 }
1110
1111 #[test]
1112 fn nested_list_partial_indent_flagged() {
1113 let content = "- Parent\n - Child\n\n continuation of parent\n";
1115 let warnings = check(content);
1116 assert_eq!(warnings.len(), 1);
1117 assert!(warnings[0].message.contains("2 spaces"));
1118 }
1119
1120 #[test]
1123 fn code_block_correctly_indented_no_warning() {
1124 let content = "- Item\n\n ```\n code\n ```\n";
1126 assert!(check(content).is_empty());
1127 }
1128
1129 #[test]
1130 fn code_fence_under_indented_warns() {
1131 let content = "- Item\n\n ```\n code\n ```\n";
1135 let warnings = check(content);
1136 assert_eq!(warnings.len(), 1);
1137 assert_eq!(warnings[0].line, 3);
1138 }
1139
1140 #[test]
1141 fn code_fence_under_indented_ordered_mkdocs() {
1142 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
1145 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
1147 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1149 assert!(warnings[0].message.contains("4 spaces"));
1150 assert!(warnings[0].message.contains("MkDocs"));
1151 }
1152
1153 #[test]
1154 fn code_fence_tilde_under_indented() {
1155 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1156 let warnings = check(content);
1157 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1159 }
1160
1161 #[test]
1164 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
1165 let content = "- Item\n\n\ncontinuation\n";
1167 assert!(check(content).is_empty());
1168 }
1169
1170 #[test]
1171 fn multiple_blank_lines_partial_indent_flags() {
1172 let content = "- Item\n\n\n continuation\n";
1173 let warnings = check(content);
1174 assert_eq!(warnings.len(), 1);
1175 }
1176
1177 #[test]
1180 fn empty_item_no_warning() {
1181 let content = "- \n- Second\n";
1182 assert!(check(content).is_empty());
1183 }
1184
1185 #[test]
1188 fn multiple_items_mixed_indent() {
1189 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
1190 let warnings = check(content);
1191 assert_eq!(warnings.len(), 1);
1192 assert_eq!(warnings[0].line, 7);
1193 }
1194
1195 #[test]
1198 fn task_list_correct_indent() {
1199 let content = "- [ ] Task\n\n continuation\n";
1201 assert!(check(content).is_empty());
1202 }
1203
1204 #[test]
1207 fn frontmatter_not_flagged() {
1208 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
1209 assert!(check(content).is_empty());
1210 }
1211
1212 #[test]
1215 fn fix_multiple_items() {
1216 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
1217 let fixed = fix(content);
1218 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
1219 }
1220
1221 #[test]
1222 fn fix_multiline_loose_continuation_all_lines() {
1223 let content = "1. Item\n\n line one\n line two\n line three\n";
1224 let fixed = fix(content);
1225 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
1226 }
1227
1228 #[test]
1231 fn sibling_item_boundary_respected() {
1232 let content = "- First\n- Second\n\n continuation\n";
1234 assert!(check(content).is_empty());
1235 }
1236
1237 #[test]
1240 fn blockquote_list_correct_indent_no_warning() {
1241 let content = "> - Item\n>\n> continuation\n";
1244 assert!(check(content).is_empty());
1245 }
1246
1247 #[test]
1248 fn blockquote_list_under_indent_no_false_positive() {
1249 let content = "> - Item\n>\n> continuation\n";
1254 assert!(check(content).is_empty());
1255 }
1256
1257 #[test]
1260 fn deeply_nested_correct_indent() {
1261 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1262 assert!(check(content).is_empty());
1263 }
1264
1265 #[test]
1266 fn deeply_nested_under_indent() {
1267 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1270 let warnings = check(content);
1271 assert_eq!(warnings.len(), 1);
1272 assert!(warnings[0].message.contains("6 spaces"));
1273 assert!(warnings[0].message.contains("found 5"));
1274 }
1275
1276 #[test]
1279 fn loose_tab_continuation_over_indented() {
1280 let content = "- Item\n\n\tcontinuation\n";
1285 let warnings = check(content);
1286 assert_eq!(warnings.len(), 1);
1287 assert_eq!(warnings[0].line, 3);
1288 assert_eq!(fix(content), "- Item\n\n continuation\n");
1289 }
1290
1291 #[test]
1294 fn multiple_continuations_correct() {
1295 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
1296 assert!(check(content).is_empty());
1297 }
1298
1299 #[test]
1300 fn multiple_continuations_second_under_indent() {
1301 let content = "- Item\n\n para 1\n\n continuation 2\n";
1303 let warnings = check(content);
1304 assert_eq!(warnings.len(), 1);
1305 assert_eq!(warnings[0].line, 5);
1306 }
1307
1308 #[test]
1311 fn ordered_paren_marker_correct() {
1312 let content = "1) Item\n\n continuation\n";
1314 assert!(check(content).is_empty());
1315 }
1316
1317 #[test]
1318 fn ordered_paren_marker_under_indent() {
1319 let content = "1) Item\n\n continuation\n";
1320 let warnings = check(content);
1321 assert_eq!(warnings.len(), 1);
1322 assert!(warnings[0].message.contains("3 spaces"));
1323 }
1324
1325 #[test]
1328 fn star_marker_correct() {
1329 let content = "* Item\n\n continuation\n";
1330 assert!(check(content).is_empty());
1331 }
1332
1333 #[test]
1334 fn star_marker_under_indent() {
1335 let content = "* Item\n\n continuation\n";
1336 let warnings = check(content);
1337 assert_eq!(warnings.len(), 1);
1338 }
1339
1340 #[test]
1341 fn plus_marker_correct() {
1342 let content = "+ Item\n\n continuation\n";
1343 assert!(check(content).is_empty());
1344 }
1345
1346 #[test]
1349 fn heading_after_list_no_warning() {
1350 let content = "- Item\n\n# Heading\n";
1351 assert!(check(content).is_empty());
1352 }
1353
1354 #[test]
1357 fn hr_after_list_no_warning() {
1358 let content = "- Item\n\n---\n";
1359 assert!(check(content).is_empty());
1360 }
1361
1362 #[test]
1365 fn reference_link_def_not_flagged() {
1366 let content = "- Item\n\n [link]: https://example.com\n";
1367 assert!(check(content).is_empty());
1368 }
1369
1370 #[test]
1373 fn footnote_def_not_flagged() {
1374 let content = "- Item\n\n [^1]: footnote text\n";
1375 assert!(check(content).is_empty());
1376 }
1377
1378 #[test]
1379 fn footnote_multiline_body_after_list_not_flagged() {
1380 let content = "# A list followed by a footnote\n\n\
1384 Here is a paragraph.[^fn]\n\n\
1385 - This is a list.\n\n\
1386 [^fn]:\n\
1387 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1388 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1389 assert!(check(content).is_empty());
1390 }
1391
1392 #[test]
1393 fn fix_footnote_multiline_body_after_list_is_noop() {
1394 let content = "# A list followed by a footnote\n\n\
1398 Here is a paragraph.[^fn]\n\n\
1399 - This is a list.\n\n\
1400 [^fn]:\n\
1401 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1402 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1403 assert_eq!(fix(content), content);
1404 }
1405
1406 #[test]
1407 fn footnote_body_indented_past_list_content_col_not_flagged() {
1408 let content = "- Item\n\n[^fn]:\n Body line one.\n Body line two.\n";
1412 assert!(check(content).is_empty());
1413 }
1414
1415 #[test]
1416 fn list_inside_footnote_body_continuation_not_flagged() {
1417 let content = "Text.[^fn]\n\n[^fn]:\n\
1421 \x20\x20\x20\x20- nested item\n\
1422 \x20\x20\x20\x20\x20\x20\x20over-indented continuation\n";
1423 assert!(check(content).is_empty());
1424 }
1425
1426 #[test]
1427 fn footnote_multiline_body_after_list_not_flagged_mkdocs() {
1428 let content = "Here is a paragraph.[^fn]\n\n\
1432 - This is a list.\n\n\
1433 [^fn]:\n\
1434 \x20\x20\x20\x20\x20\x20Footnote body that spans\n\
1435 \x20\x20\x20\x20\x20\x20multiple indented lines.\n";
1436 assert!(check_mkdocs(content).is_empty());
1437 }
1438
1439 #[test]
1442 fn fix_deeply_nested() {
1443 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
1444 let fixed = fix(content);
1445 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
1446 }
1447
1448 #[test]
1449 fn fix_mkdocs_unordered() {
1450 let content = "- Item\n\n continuation\n";
1452 let fixed = fix_mkdocs(content);
1453 assert_eq!(fixed, "- Item\n\n continuation\n");
1454 }
1455
1456 #[test]
1457 fn fix_code_fence_indent() {
1458 let content = "- Item\n\n ```\n code\n ```\n";
1461 let fixed = fix(content);
1462 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
1463 }
1464
1465 #[test]
1466 fn fix_mkdocs_code_fence_indent() {
1467 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
1469 let fixed = fix_mkdocs(content);
1470 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
1471 }
1472
1473 #[test]
1476 fn empty_document_no_warning() {
1477 assert!(check("").is_empty());
1478 }
1479
1480 #[test]
1481 fn whitespace_only_no_warning() {
1482 assert!(check(" \n\n \n").is_empty());
1483 }
1484
1485 #[test]
1488 fn no_list_no_warning() {
1489 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
1490 assert!(check(content).is_empty());
1491 }
1492
1493 #[test]
1496 fn multiline_continuation_all_lines_flagged() {
1497 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";
1498 let warnings = check(content);
1499 assert_eq!(warnings.len(), 3);
1500 assert_eq!(warnings[0].line, 3);
1501 assert_eq!(warnings[1].line, 4);
1502 assert_eq!(warnings[2].line, 5);
1503 }
1504
1505 #[test]
1506 fn multiline_continuation_with_frontmatter_fix() {
1507 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";
1508 let fixed = fix(content);
1509 assert_eq!(
1510 fixed,
1511 "---\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"
1512 );
1513 }
1514
1515 #[test]
1516 fn multiline_continuation_correct_indent_no_warning() {
1517 let content = "1. Item\n\n line one\n line two\n line three\n";
1518 assert!(check(content).is_empty());
1519 }
1520
1521 #[test]
1522 fn multiline_continuation_mixed_indent() {
1523 let content = "1. Item\n\n correct\n wrong\n correct\n";
1524 let warnings = check(content);
1525 assert_eq!(warnings.len(), 1);
1526 assert_eq!(warnings[0].line, 4);
1527 }
1528
1529 #[test]
1530 fn multiline_continuation_unordered() {
1531 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
1532 let warnings = check(content);
1533 assert_eq!(warnings.len(), 3);
1534 let fixed = fix(content);
1535 assert_eq!(
1536 fixed,
1537 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
1538 );
1539 }
1540
1541 #[test]
1542 fn multiline_continuation_two_items_fix() {
1543 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
1544 let fixed = fix(content);
1545 assert_eq!(
1546 fixed,
1547 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1548 );
1549 }
1550
1551 #[test]
1552 fn fence_fix_does_not_break_pairing_for_md031() {
1553 let content = "#### title\n\nabc\n\n\
1560 1. ab\n\n\
1561 \x20\x20`aabbccdd`\n\n\
1562 2. cd\n\n\
1563 \x20\x20`bbcc dd ee`\n\n\
1564 \x20\x20```\n\
1565 \x20\x20abcd\n\
1566 \x20\x20ef gh\n\
1567 \x20\x20```\n\n\
1568 \x20\x20uu\n\n\
1569 \x20\x20```\n\
1570 \x20\x20cdef\n\
1571 \x20\x20gh ij\n\
1572 \x20\x20```\n";
1573 let expected = "#### title\n\nabc\n\n\
1574 1. ab\n\n\
1575 \x20\x20\x20`aabbccdd`\n\n\
1576 2. cd\n\n\
1577 \x20\x20\x20`bbcc dd ee`\n\n\
1578 \x20\x20\x20```\n\
1579 \x20\x20\x20abcd\n\
1580 \x20\x20\x20ef gh\n\
1581 \x20\x20\x20```\n\n\
1582 \x20\x20\x20uu\n\n\
1583 \x20\x20\x20```\n\
1584 \x20\x20\x20cdef\n\
1585 \x20\x20\x20gh ij\n\
1586 \x20\x20\x20```\n";
1587 assert_eq!(fix(content), expected);
1588 }
1589
1590 #[test]
1591 fn multiline_continuation_separated_by_blank() {
1592 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1593 let warnings = check(content);
1594 assert_eq!(warnings.len(), 4);
1595 let fixed = fix(content);
1596 assert_eq!(
1597 fixed,
1598 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1599 );
1600 }
1601
1602 #[test]
1603 fn tab_indented_fence_is_normalized_to_spaces() {
1604 let content = "100. ab\n\n\t```\n\tabcd\n\t```\n";
1612 let expected = "100. ab\n\n ```\n abcd\n ```\n";
1613 assert_eq!(fix(content), expected);
1614 }
1615
1616 #[test]
1625 fn loose_continuation_over_indented_flagged() {
1626 let content = "* Item\n\n over-indented\n";
1629 let warnings = check(content);
1630 assert_eq!(warnings.len(), 1);
1631 assert_eq!(warnings[0].line, 3);
1632 assert!(warnings[0].message.contains("over-indented"));
1633 assert!(warnings[0].message.contains("expected 2"));
1634 assert!(warnings[0].message.contains("found 3"));
1635 }
1636
1637 #[test]
1638 fn loose_continuation_over_indented_multiline_mixed() {
1639 let content = "* Item\n\n over one\n correct\n over two\n";
1641 let warnings = check(content);
1642 assert_eq!(warnings.len(), 2);
1643 assert_eq!(warnings[0].line, 3);
1644 assert_eq!(warnings[1].line, 5);
1645 }
1646
1647 #[test]
1648 fn fix_loose_continuation_over_indented() {
1649 let content = "* Item\n\n over one\n correct\n over two\n";
1650 let fixed = fix(content);
1651 assert_eq!(fixed, "* Item\n\n over one\n correct\n over two\n");
1652 }
1653
1654 #[test]
1655 fn fix_tight_and_loose_items_normalized_identically() {
1656 let content = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1659 * 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\
1660 * 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";
1661 let expected = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1662 * 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\
1663 * 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";
1664 assert_eq!(fix(content), expected);
1665 }
1666
1667 #[test]
1668 fn multi_paragraph_item_loose_paragraph_over_indented() {
1669 let content = "* Item.\n tight over\n\n loose over\n";
1672 let warnings = check(content);
1673 assert_eq!(warnings.len(), 2);
1674 assert_eq!(warnings[0].line, 2);
1675 assert_eq!(warnings[1].line, 4);
1676 }
1677
1678 #[test]
1679 fn loose_indented_code_block_not_flagged() {
1680 let content = "- Item\n\n code line\n";
1684 assert!(check(content).is_empty());
1685 }
1686
1687 #[test]
1688 fn mkdocs_loose_over_indented_flagged() {
1689 let content = "1. Item\n\n over\n";
1692 let warnings = check_mkdocs(content);
1693 assert_eq!(warnings.len(), 1);
1694 assert_eq!(warnings[0].line, 3);
1695 assert!(warnings[0].message.contains("over-indented"));
1696 assert!(warnings[0].message.contains("expected 4"));
1697 assert!(warnings[0].message.contains("found 5"));
1698 }
1699
1700 #[test]
1701 fn task_list_loose_over_indented_flagged() {
1702 let content = "- [ ] Task\n\n over\n";
1705 let warnings = check(content);
1706 assert_eq!(warnings.len(), 1);
1707 assert_eq!(warnings[0].line, 3);
1708 }
1709
1710 #[test]
1711 fn loose_over_indent_boundary_below_code_block_threshold_flagged() {
1712 let content = "- Item\n\n over\n";
1717 let warnings = check(content);
1718 assert_eq!(warnings.len(), 1);
1719 assert_eq!(warnings[0].line, 3);
1720 assert!(warnings[0].message.contains("expected 2"));
1721 assert!(warnings[0].message.contains("found 5"));
1722 }
1723
1724 #[test]
1725 fn loose_over_indent_does_not_steal_nested_under_indent() {
1726 let content = "- Outer\n - Inner\n\n continuation\n";
1733 let warnings = check(content);
1734 assert_eq!(warnings.len(), 1);
1735 assert_eq!(warnings[0].line, 4);
1736 assert!(warnings[0].message.contains("4 spaces"));
1737 assert!(warnings[0].message.contains("found 3"));
1738 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1739 }
1740
1741 #[test]
1742 fn loose_over_indent_attributes_to_deepest_enclosing_item() {
1743 let content = "- Outer\n - Inner\n\n continuation\n";
1747 let warnings = check(content);
1748 assert_eq!(warnings.len(), 1);
1749 assert_eq!(warnings[0].line, 4);
1750 assert!(warnings[0].message.contains("expected 4"));
1751 assert!(warnings[0].message.contains("found 5"));
1752 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1753 }
1754
1755 #[test]
1764 fn loose_over_indented_fence_not_flagged() {
1765 let content = "- Item\n\n ```\n code\n ```\n";
1766 assert!(check(content).is_empty());
1767 assert_eq!(fix(content), content);
1768 }
1769
1770 #[test]
1771 fn tight_over_indented_fence_not_flagged() {
1772 let content = "- Item\n ```\n code\n ```\n";
1773 assert!(check(content).is_empty());
1774 assert_eq!(fix(content), content);
1775 }
1776
1777 #[test]
1778 fn over_indented_tilde_fence_not_flagged() {
1779 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1780 assert!(check(content).is_empty());
1781 assert_eq!(fix(content), content);
1782 }
1783
1784 #[test]
1785 fn fence_like_code_content_inside_fenced_block_not_flagged() {
1786 let content = "- Item\n\n ~~~\n ```\n ~~~\n";
1789 assert!(check(content).is_empty());
1790 assert_eq!(fix(content), content);
1791 }
1792
1793 #[test]
1794 fn unterminated_over_indented_fence_not_flagged() {
1795 let content = "- Item\n\n ```\n code1\n code2deeper\n";
1798 assert!(check(content).is_empty());
1799 assert_eq!(fix(content), content);
1800 }
1801
1802 #[test]
1810 fn task_list_tight_continuation_post_checkbox_reproducer_579() {
1811 let content = "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n";
1814 assert!(check(content).is_empty());
1815 }
1816
1817 #[test]
1818 fn task_list_tight_continuation_dash_unchecked() {
1819 let content = "- [ ] Task\n continuation\n";
1820 assert!(check(content).is_empty());
1821 }
1822
1823 #[test]
1824 fn task_list_tight_continuation_dash_checked_lower() {
1825 let content = "- [x] Task\n continuation\n";
1826 assert!(check(content).is_empty());
1827 }
1828
1829 #[test]
1830 fn task_list_tight_continuation_dash_checked_upper() {
1831 let content = "- [X] Task\n continuation\n";
1832 assert!(check(content).is_empty());
1833 }
1834
1835 #[test]
1836 fn task_list_tight_continuation_star_marker() {
1837 let content = "* [ ] Task\n continuation\n";
1838 assert!(check(content).is_empty());
1839 }
1840
1841 #[test]
1842 fn task_list_tight_continuation_plus_marker() {
1843 let content = "+ [ ] Task\n continuation\n";
1844 assert!(check(content).is_empty());
1845 }
1846
1847 #[test]
1848 fn task_list_tight_continuation_content_column_still_valid() {
1849 let content = "- [ ] Task\n continuation\n";
1852 assert!(check(content).is_empty());
1853 }
1854
1855 #[test]
1856 fn task_list_tight_continuation_between_columns_still_flagged() {
1857 let content = "- [ ] Task\n continuation\n";
1860 let warnings = check(content);
1861 assert_eq!(warnings.len(), 1);
1862 assert!(warnings[0].message.contains("expected 2 or 6"));
1864 assert!(warnings[0].message.contains("found 4"));
1865 }
1866
1867 #[test]
1868 fn task_list_tight_continuation_overshoot_still_flagged() {
1869 let content = "- [ ] Task\n continuation\n";
1871 let warnings = check(content);
1872 assert_eq!(warnings.len(), 1);
1873 assert!(warnings[0].message.contains("expected 2 or 6"));
1874 assert!(warnings[0].message.contains("found 7"));
1875 }
1876
1877 #[test]
1880 fn fix_task_list_overshoot_snaps_to_task_col() {
1881 let content = "- [ ] Task\n continuation\n";
1885 let fixed = fix(content);
1886 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1887 }
1888
1889 #[test]
1890 fn fix_task_list_col_5_snaps_to_task_col() {
1891 let content = "- [ ] Task\n continuation\n";
1893 let fixed = fix(content);
1894 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1895 }
1896
1897 #[test]
1898 fn fix_task_list_col_3_snaps_to_content_col() {
1899 let content = "- [ ] Task\n continuation\n";
1901 let fixed = fix(content);
1902 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1903 }
1904
1905 #[test]
1906 fn fix_task_list_col_4_ties_to_content_col() {
1907 let content = "- [ ] Task\n continuation\n";
1912 let fixed = fix(content);
1913 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1914 }
1915
1916 #[test]
1917 fn fix_task_list_ordered_overshoot_snaps_to_task_col() {
1918 let content = "1. [ ] Task\n continuation\n";
1921 let fixed = fix(content);
1922 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1923 }
1924
1925 #[test]
1926 fn fix_task_list_ordered_under_overshoot_snaps_to_content_col() {
1927 let content = "1. [ ] Task\n continuation\n";
1930 let fixed = fix(content);
1931 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1932 }
1933
1934 #[test]
1935 fn task_list_tight_continuation_ordered_single_digit() {
1936 let content = "1. [ ] Task\n continuation\n";
1938 assert!(check(content).is_empty());
1939 }
1940
1941 #[test]
1942 fn task_list_tight_continuation_ordered_multi_digit() {
1943 let content = "10. [ ] Task\n continuation\n";
1945 assert!(check(content).is_empty());
1946 }
1947
1948 #[test]
1949 fn task_list_tight_continuation_nested_dash() {
1950 let content = "- Parent\n - [ ] Nested task\n continuation\n";
1952 assert!(check(content).is_empty());
1953 }
1954
1955 #[test]
1956 fn task_list_loose_continuation_post_checkbox_column_not_flagged() {
1957 let content = "- [ ] Task\n\n continuation\n";
1962 assert!(check(content).is_empty());
1963 }
1964
1965 #[test]
1966 fn task_list_empty_body_is_not_a_task() {
1967 let content = "- [ ]\n continuation\n";
1973 let warnings = check(content);
1974 assert_eq!(warnings.len(), 1);
1975 assert!(warnings[0].message.contains("found 4"));
1976 }
1977
1978 #[test]
1979 fn task_list_malformed_checkbox_is_not_a_task() {
1980 let content = "- [~] Not a task\n continuation\n";
1982 let warnings = check(content);
1983 assert_eq!(warnings.len(), 1);
1984 }
1985
1986 #[test]
1993 fn task_list_mkdocs_unordered_required_min_valid() {
1994 let content = "- [ ] Task\n continuation\n";
1996 assert!(check_mkdocs(content).is_empty());
1997 }
1998
1999 #[test]
2000 fn task_list_mkdocs_unordered_post_checkbox_valid() {
2001 let content = "- [ ] Task\n continuation\n";
2002 assert!(check_mkdocs(content).is_empty());
2003 }
2004
2005 #[test]
2006 fn task_list_mkdocs_unordered_between_flagged() {
2007 let content = "- [ ] Task\n continuation\n";
2009 let warnings = check_mkdocs(content);
2010 assert_eq!(warnings.len(), 1);
2011 }
2012
2013 #[test]
2014 fn task_list_mkdocs_ordered_both_columns_valid() {
2015 let at_4 = "1. [ ] Task\n continuation\n";
2017 assert!(check_mkdocs(at_4).is_empty());
2018 let at_7 = "1. [ ] Task\n continuation\n";
2019 assert!(check_mkdocs(at_7).is_empty());
2020 }
2021
2022 #[test]
2023 fn task_list_mkdocs_ordered_between_flagged() {
2024 let at_5 = "1. [ ] Task\n continuation\n";
2026 assert_eq!(check_mkdocs(at_5).len(), 1);
2027 let at_6 = "1. [ ] Task\n continuation\n";
2028 assert_eq!(check_mkdocs(at_6).len(), 1);
2029 }
2030
2031 #[test]
2041 fn fix_task_list_tie_sibling_at_task_col_snaps_to_task_col() {
2042 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
2046 let fixed = fix(content);
2047 assert_eq!(
2048 fixed,
2049 "- [ ] Task\n aligned continuation\n tied continuation\n"
2050 );
2051 }
2052
2053 #[test]
2054 fn fix_task_list_tie_sibling_at_content_col_snaps_to_content_col() {
2055 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
2058 let fixed = fix(content);
2059 assert_eq!(fixed, "- [ ] Task\n aligned continuation\n tied continuation\n");
2060 }
2061
2062 #[test]
2063 fn fix_task_list_tie_both_siblings_snaps_to_content_col() {
2064 let content = "- [ ] Task\n at content col\n at task col\n tied continuation\n";
2068 let fixed = fix(content);
2069 assert_eq!(
2070 fixed,
2071 "- [ ] Task\n at content col\n at task col\n tied continuation\n"
2072 );
2073 }
2074
2075 #[test]
2076 fn fix_task_list_tie_sees_task_col_through_tight_lazy_continuation() {
2077 let content = concat!("- [ ] Task\n", "lazy\n", " aligned at task col\n", " tied\n",);
2091 let fixed = fix(content);
2092 assert!(
2093 fixed.contains("\n tied\n"),
2094 "tied line should snap to col 6 (task col) because a task-col \
2095 sibling is visible past the tight lazy-continuation line; got:\n{fixed}"
2096 );
2097 }
2098
2099 #[test]
2106 fn task_list_tab_indented_continuation_flagged() {
2107 let content = "- [ ] Task\n\t\twrap\n";
2110 let warnings = check(content);
2111 assert_eq!(warnings.len(), 1);
2112 assert!(warnings[0].message.contains("expected 2 or 6"));
2113 assert!(warnings[0].message.contains("found 8"));
2114 }
2115
2116 #[test]
2117 fn fix_task_list_tab_indented_snaps_to_task_col() {
2118 let content = "- [ ] Task\n\t\twrap\n";
2120 let fixed = fix(content);
2121 assert_eq!(fixed, "- [ ] Task\n wrap\n");
2122 }
2123
2124 #[test]
2125 fn fix_task_list_single_tab_equidistant_snaps_to_content_col() {
2126 let content = "- [ ] Task\n\twrap\n";
2129 let fixed = fix(content);
2130 assert_eq!(fixed, "- [ ] Task\n wrap\n");
2131 }
2132
2133 #[test]
2143 fn task_list_blockquote_post_checkbox_not_flagged() {
2144 let content = "> - [ ] Task\n> continuation\n";
2146 assert!(check(content).is_empty());
2147 }
2148
2149 #[test]
2150 fn task_list_blockquote_between_cols_documented_limitation() {
2151 let content = "> - [ ] Task\n> continuation\n";
2155 assert!(check(content).is_empty());
2156 }
2157
2158 #[test]
2159 fn task_list_blockquote_overshoot_documented_limitation() {
2160 let content = "> - [ ] Task\n> continuation\n";
2162 assert!(check(content).is_empty());
2163 }
2164
2165 #[test]
2172 fn fix_task_list_mkdocs_unordered_overshoot_snaps_to_task_col() {
2173 let content = "- [ ] Task\n continuation\n";
2176 let fixed = fix_mkdocs(content);
2177 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2178 }
2179
2180 #[test]
2181 fn fix_task_list_mkdocs_unordered_tie_snaps_to_required() {
2182 let content = "- [ ] Task\n continuation\n";
2185 let fixed = fix_mkdocs(content);
2186 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2187 }
2188
2189 #[test]
2190 fn fix_task_list_mkdocs_ordered_overshoot_snaps_to_task_col() {
2191 let content = "1. [ ] Task\n continuation\n";
2194 let fixed = fix_mkdocs(content);
2195 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2196 }
2197
2198 #[test]
2199 fn fix_task_list_mkdocs_ordered_near_required_snaps_to_required() {
2200 let content = "1. [ ] Task\n continuation\n";
2206 let fixed = fix_mkdocs(content);
2207 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2208 }
2209
2210 #[test]
2211 fn fix_task_list_mkdocs_ordered_between_cols_snaps_to_task_col() {
2212 let content = "1. [ ] Task\n continuation\n";
2215 let fixed = fix_mkdocs(content);
2216 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2217 }
2218
2219 fn assert_idempotent(content: &str) {
2229 let once = fix(content);
2230 let twice = fix(&once);
2231 assert_eq!(once, twice, "MD077 fix was not idempotent on input: {content:?}");
2232 }
2233
2234 fn assert_idempotent_mkdocs(content: &str) {
2235 let once = fix_mkdocs(content);
2236 let twice = fix_mkdocs(&once);
2237 assert_eq!(
2238 once, twice,
2239 "MD077 (MkDocs) fix was not idempotent on input: {content:?}"
2240 );
2241 }
2242
2243 #[test]
2244 fn idempotent_task_list_between_cols() {
2245 assert_idempotent("- [ ] Task\n continuation\n");
2246 }
2247
2248 #[test]
2249 fn idempotent_task_list_overshoot() {
2250 assert_idempotent("- [ ] Task\n continuation\n");
2251 }
2252
2253 #[test]
2254 fn idempotent_task_list_under_post_checkbox() {
2255 assert_idempotent("- [ ] Task\n continuation\n");
2256 }
2257
2258 #[test]
2259 fn idempotent_task_list_near_post_checkbox() {
2260 assert_idempotent("- [ ] Task\n continuation\n");
2261 }
2262
2263 #[test]
2264 fn idempotent_task_list_tab_overshoot() {
2265 assert_idempotent("- [ ] Task\n\t\twrap\n");
2266 }
2267
2268 #[test]
2269 fn idempotent_task_list_single_tab() {
2270 assert_idempotent("- [ ] Task\n\twrap\n");
2271 }
2272
2273 #[test]
2274 fn idempotent_task_list_ordered_overshoot() {
2275 assert_idempotent("1. [ ] Task\n continuation\n");
2276 }
2277
2278 #[test]
2279 fn idempotent_task_list_ordered_under() {
2280 assert_idempotent("1. [ ] Task\n continuation\n");
2281 }
2282
2283 #[test]
2284 fn idempotent_task_list_tie_with_sibling_at_task_col() {
2285 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2286 }
2287
2288 #[test]
2289 fn idempotent_task_list_tie_with_sibling_at_content_col() {
2290 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2291 }
2292
2293 #[test]
2294 fn idempotent_task_list_mkdocs_unordered_overshoot() {
2295 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2296 }
2297
2298 #[test]
2299 fn idempotent_task_list_mkdocs_unordered_tie() {
2300 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2301 }
2302
2303 #[test]
2304 fn idempotent_task_list_mkdocs_ordered_overshoot() {
2305 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2306 }
2307
2308 #[test]
2309 fn idempotent_task_list_mkdocs_ordered_between() {
2310 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2311 }
2312
2313 #[test]
2314 fn idempotent_task_list_reproducer_579() {
2315 assert_idempotent(
2319 "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n",
2320 );
2321 }
2322
2323 #[test]
2324 fn idempotent_non_task_list_still_holds() {
2325 assert_idempotent("1. Item\n over-indented\n");
2328 assert_idempotent("- Item\n\n continuation\n");
2329 }
2330
2331 #[test]
2338 fn idempotent_non_task_loose_under_indent_ordered() {
2339 assert_idempotent("1. Item\n\n continuation\n");
2341 }
2342
2343 #[test]
2344 fn idempotent_non_task_loose_under_indent_multi_digit() {
2345 assert_idempotent("10. Item\n\n continuation\n");
2347 }
2348
2349 #[test]
2350 fn idempotent_non_task_tight_over_indent_ordered() {
2351 assert_idempotent("1. Item\n over-indented\n");
2353 }
2354
2355 #[test]
2363 fn idempotent_non_task_fence_ordered_loose() {
2364 assert_idempotent("1. Item\n\n ```rust\n let x = 1;\n ```\n");
2366 }
2367
2368 #[test]
2369 fn idempotent_non_task_fence_tilde_under_indent() {
2370 assert_idempotent("1. Item\n\n ~~~\nplain text\n ~~~\n");
2376 }
2377
2378 #[test]
2379 fn idempotent_non_task_fence_interior_above_required() {
2380 assert_idempotent("1. Item\n\n ```\n deeply indented code\n ```\n");
2384 }
2385
2386 #[test]
2387 fn fence_fix_promotes_interior_below_scope_in_single_pass() {
2388 let content = "1. Item\n\n ```\ncode\n ```\n";
2392 let fixed = fix(content);
2393 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2394 }
2395
2396 #[test]
2397 fn fence_fix_preserves_interior_above_required() {
2398 let content = "1. Item\n\n ```\n code\n ```\n";
2401 let fixed = fix(content);
2402 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2403 }
2404
2405 #[test]
2412 fn idempotent_non_task_mkdocs_ordered_at_3_spaces() {
2413 assert_idempotent_mkdocs("1. Item\n\n continuation\n");
2415 }
2416
2417 #[test]
2418 fn idempotent_non_task_mkdocs_unordered_at_2_spaces() {
2419 assert_idempotent_mkdocs("- Item\n\n continuation\n");
2421 }
2422
2423 #[test]
2424 fn idempotent_non_task_mkdocs_fence_compound() {
2425 assert_idempotent_mkdocs("1. Item\n\n ```toml\n k = 1\n ```\n");
2427 }
2428
2429 #[test]
2432 fn aligned_tight_zero_indent_continuation_flagged() {
2433 let content = "- this is a long line\nthat continues on a second line\n";
2437 let warnings = check_aligned(content);
2438 assert_eq!(warnings.len(), 1);
2439 assert_eq!(warnings[0].line, 2);
2440 assert_eq!(
2441 fix_aligned(content),
2442 "- this is a long line\n that continues on a second line\n"
2443 );
2444 }
2445
2446 #[test]
2447 fn aligned_full_issue_example_made_consistent() {
2448 let content = "- this is a long line\n\
2451 that continues on a second line\n\
2452 - this is another long line\n\
2453 \x20\x20that continues on the next line\n\
2454 - yet again a long line\n\
2455 and still inconsistently spaced\n\
2456 \x20\x20and even worse\n";
2457 let expected = "- this is a long line\n\
2458 \x20\x20that continues on a second line\n\
2459 - this is another long line\n\
2460 \x20\x20that continues on the next line\n\
2461 - yet again a long line\n\
2462 \x20\x20and still inconsistently spaced\n\
2463 \x20\x20and even worse\n";
2464 assert_eq!(fix_aligned(content), expected);
2465 assert_eq!(fix_aligned(expected), expected);
2467 }
2468
2469 #[test]
2470 fn aligned_already_aligned_not_flagged() {
2471 let content = "- item\n continuation at content column\n";
2472 assert!(check_aligned(content).is_empty());
2473 }
2474
2475 #[test]
2476 fn aligned_tight_partial_indent_flagged() {
2477 let content = "- item\n continuation\n";
2479 let warnings = check_aligned(content);
2480 assert_eq!(warnings.len(), 1);
2481 assert_eq!(fix_aligned(content), "- item\n continuation\n");
2482 }
2483
2484 #[test]
2485 fn aligned_post_blank_zero_indent_still_new_paragraph() {
2486 let content = "- item\n\nnew paragraph\n";
2489 assert!(check_aligned(content).is_empty());
2490 assert_eq!(fix_aligned(content), content);
2491 }
2492
2493 #[test]
2496 fn aligned_top_level_blockquote_after_list_untouched() {
2497 let content = "- item\n> quote\n";
2501 assert!(check_aligned(content).is_empty());
2502 assert_eq!(fix_aligned(content), content);
2503 }
2504
2505 #[test]
2506 fn aligned_top_level_fence_after_list_untouched() {
2507 let content = "- item\n```\ncode\n```\n";
2508 assert!(check_aligned(content).is_empty());
2509 assert_eq!(fix_aligned(content), content);
2510 }
2511
2512 #[test]
2513 fn aligned_top_level_table_after_list_untouched() {
2514 let content = "- item\n| a | b |\n|---|---|\n| 1 | 2 |\n";
2515 assert!(check_aligned(content).is_empty());
2516 assert_eq!(fix_aligned(content), content);
2517 }
2518
2519 #[test]
2522 fn aligned_nested_tight_lazy_continuation_aligns_to_inner() {
2523 let content = "- Outer\n - Inner\ncontinuation\n";
2528 let warnings = check_aligned(content);
2529 assert_eq!(warnings.len(), 1);
2530 assert_eq!(fix_aligned(content), "- Outer\n - Inner\n continuation\n");
2531 }
2532
2533 #[test]
2534 fn aligned_nested_continuation_already_aligned_not_flagged() {
2535 let content = "- L1\n - L2\n cont of L2 at 4\n";
2536 assert!(check_aligned(content).is_empty());
2537 }
2538
2539 #[test]
2540 fn aligned_nested_idempotent() {
2541 let content = "- Outer\n - Inner\ncontinuation\n";
2542 let once = fix_aligned(content);
2543 assert_eq!(fix_aligned(&once), once);
2544 }
2545
2546 #[test]
2547 fn aligned_three_level_nesting_aligns_to_innermost() {
2548 let content = "- L1\n - L2\n - L3\ncont\n";
2551 assert_eq!(fix_aligned(content), "- L1\n - L2\n - L3\n cont\n");
2552 }
2553
2554 #[test]
2555 fn aligned_continuation_after_sibling_owned_by_last_item() {
2556 let content = "- a\n- b\nlazy\n";
2559 assert_eq!(fix_aligned(content), "- a\n- b\n lazy\n");
2560 }
2561
2562 #[test]
2563 fn aligned_multi_digit_ordered_marker_aligns_to_content_column() {
2564 let content = "10. Item\nwrap\n";
2565 assert_eq!(fix_aligned(content), "10. Item\n wrap\n");
2566 }
2567
2568 #[test]
2569 fn aligned_setext_heading_after_list_left_alone() {
2570 let content = "- item\nText\n===\n";
2573 assert!(check_aligned(content).is_empty());
2574 assert_eq!(fix_aligned(content), content);
2575 }
2576
2577 #[test]
2578 fn aligned_latent_marker_in_continuation_is_idempotent() {
2579 let content = "# \n- \n``\n2. \n![]()";
2585 let once = fix_aligned(content);
2586 assert_eq!(fix_aligned(&once), once, "fix must be idempotent in one pass");
2587 assert_eq!(once, content, "item with a latent marker is left untouched");
2588 }
2589
2590 #[test]
2591 fn aligned_latent_table_in_continuation_is_idempotent() {
2592 let content = "- \n![`]()\n| | ` |\n| --- | --- |";
2597 let once = fix_aligned(content);
2598 assert_eq!(fix_aligned(&once), once, "fix must be idempotent in one pass");
2599 assert_eq!(once, content, "item with a latent table is left untouched");
2600 }
2601
2602 #[test]
2603 fn aligned_blockquote_nested_list_not_touched() {
2604 let content = "> - item\n> wrap\n";
2608 assert!(check_aligned(content).is_empty());
2609 assert_eq!(fix_aligned(content), content);
2610 }
2611
2612 #[test]
2615 fn aligned_task_post_checkbox_column_accepted() {
2616 let content = "- [ ] Task\n wrap\n";
2619 assert!(check_aligned(content).is_empty());
2620 assert_eq!(fix_aligned(content), content);
2621 }
2622
2623 #[test]
2624 fn aligned_task_under_indent_snaps_to_content_column() {
2625 let content = "- [ ] Task\nwrap\n";
2626 let warnings = check_aligned(content);
2627 assert_eq!(warnings.len(), 1);
2628 assert_eq!(fix_aligned(content), "- [ ] Task\n wrap\n");
2629 }
2630
2631 #[test]
2634 fn aligned_mkdocs_tight_under_indent_snaps_to_four() {
2635 let content = "- item\nwrap\n";
2637 let warnings = check_aligned_mkdocs(content);
2638 assert_eq!(warnings.len(), 1);
2639 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
2640 assert_eq!(aligned_rule().fix(&ctx).unwrap(), "- item\n wrap\n");
2641 }
2642
2643 #[test]
2646 fn any_default_does_not_flag_tight_lazy_continuation() {
2647 let content = "- item\nwrapped at zero indent\n";
2649 assert!(check(content).is_empty());
2650 assert_eq!(fix(content), content);
2651 }
2652
2653 #[test]
2654 fn from_config_aligned_enables_tight_flagging() {
2655 let mut config = crate::config::Config::default();
2657 let mut rule_config = crate::config::RuleConfig::default();
2658 rule_config
2659 .values
2660 .insert("style".to_string(), toml::Value::String("aligned".to_string()));
2661 config.rules.insert("MD077".to_string(), rule_config);
2662
2663 let rule = MD077ListContinuationIndent::from_config(&config);
2664 let ctx = LintContext::new("- item\nwrap\n", MarkdownFlavor::Standard, None);
2665 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
2666 }
2667
2668 #[test]
2669 fn from_config_default_is_any() {
2670 let config = crate::config::Config::default();
2672 let rule = MD077ListContinuationIndent::from_config(&config);
2673 let ctx = LintContext::new("- item\nwrap\n", MarkdownFlavor::Standard, None);
2674 assert!(rule.check(&ctx).unwrap().is_empty());
2675 }
2676
2677 #[test]
2678 fn aligned_tight_underindented_fence_inside_item_left_alone() {
2679 let content = "- item\n ```\n code\n ```\n";
2683 assert!(check_aligned(content).is_empty());
2684 assert_eq!(fix_aligned(content), content);
2685 }
2686
2687 #[test]
2688 fn aligned_task_under_indent_fix_is_idempotent() {
2689 let content = "- [ ] Task\nwrap\n";
2690 let once = fix_aligned(content);
2691 assert_eq!(fix_aligned(&once), once);
2692 }
2693
2694 #[test]
2695 fn aligned_partial_indent_fix_is_idempotent() {
2696 let content = "- item\n continuation\n";
2697 let once = fix_aligned(content);
2698 assert_eq!(fix_aligned(&once), once);
2699 }
2700}