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 item_range_has_latent_structure(ctx: &LintContext, item_line: usize, range_end: usize) -> bool {
343 (item_line + 1..=range_end).any(|line_num| {
344 ctx.line_info(line_num).is_some_and(|info| {
345 if info.is_blank || info.list_item.is_some() {
346 return false;
347 }
348 let trimmed = info.content(ctx.content).trim_start();
349 !Self::should_skip_line(info, trimmed)
350 && (Self::starts_with_list_marker(trimmed) || crate::utils::skip_context::is_table_line(trimmed))
351 })
352 })
353 }
354
355 fn sibling_column_usage(
365 ctx: &LintContext,
366 item_line: usize,
367 range_end: usize,
368 marker_col: usize,
369 content_col: usize,
370 task_col: usize,
371 ) -> (bool, bool) {
372 let mut uses_content = false;
373 let mut uses_task = false;
374
375 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
376 if line.actual == content_col {
377 uses_content = true;
378 }
379 if line.actual == task_col {
380 uses_task = true;
381 }
382 if uses_content && uses_task {
383 ControlFlow::Break(())
384 } else {
385 ControlFlow::Continue(())
386 }
387 });
388
389 (uses_content, uses_task)
390 }
391
392 fn compute_fix_target(
398 actual: usize,
399 required: usize,
400 task_col: Option<usize>,
401 uses_content_col: bool,
402 uses_task_col: bool,
403 ) -> usize {
404 let Some(t) = task_col else { return required };
405 match actual.abs_diff(t).cmp(&actual.abs_diff(required)) {
406 std::cmp::Ordering::Less => t,
407 std::cmp::Ordering::Greater => required,
408 std::cmp::Ordering::Equal => match (uses_task_col, uses_content_col) {
409 (true, false) => t,
410 _ => required,
411 },
412 }
413 }
414
415 fn should_skip_line(info: &crate::lint_context::LineInfo, trimmed: &str) -> bool {
426 if info.in_code_block && !Self::is_code_fence(trimmed) {
427 return true;
428 }
429 info.in_front_matter
430 || info.in_footnote_definition
431 || info.in_html_block
432 || info.in_html_comment
433 || info.in_mdx_comment
434 || info.in_mkdocstrings
435 || info.in_esm_block
436 || info.in_math_block
437 || info.in_admonition
438 || info.in_content_tab
439 || info.in_pymdown_block
440 || info.in_definition_list
441 || info.in_mkdocs_html_markdown
442 || info.in_kramdown_extension_block
443 }
444
445 fn build_over_indent_warning(
454 ctx: &LintContext,
455 line: &ContinuationLine<'_>,
456 fix_target: usize,
457 message: String,
458 ) -> LintWarning {
459 let line_content = line.info.content(ctx.content);
460 let fix_start = line.info.byte_offset;
461 let fix_end = fix_start + line.info.indent;
462 LintWarning {
463 rule_name: Some("MD077".to_string()),
464 line: line.line_num,
465 column: 1,
466 end_line: line.line_num,
467 end_column: line_content.chars().count() + 1,
468 message,
469 severity: Severity::Warning,
470 fix: Some(Fix::new(fix_start..fix_end, " ".repeat(fix_target))),
471 }
472 }
473
474 fn build_under_indent_warning(
486 ctx: &LintContext,
487 line: &ContinuationLine<'_>,
488 required: usize,
489 message: String,
490 ) -> UnderIndentOutcome {
491 let line_content = line.info.content(ctx.content);
492 let is_fence_opener = line.info.in_code_block
493 && Self::is_code_fence(line.trimmed)
494 && ctx.line_info(line.line_num - 1).is_none_or(|p| !p.in_code_block);
495
496 let (fix, warn_end_line, warn_end_column, compound_closer) = if is_fence_opener {
497 let closer_line = Self::find_fence_closer(ctx, line.line_num);
498 let fix = Self::build_compound_fence_fix(ctx, line.line_num, closer_line, line.actual, required);
499 let end_column = ctx
500 .line_info(closer_line)
501 .map_or(line_content.chars().count() + 1, |ci| {
502 ci.content(ctx.content).chars().count() + 1
503 });
504 let extra_flag = (closer_line != line.line_num).then_some(closer_line);
505 (fix, closer_line, end_column, extra_flag)
506 } else {
507 let fix_start = line.info.byte_offset;
508 let fix_end = fix_start + line.info.indent;
509 let fix = Some(Fix::new(fix_start..fix_end, " ".repeat(required)));
510 (fix, line.line_num, line_content.chars().count() + 1, None)
511 };
512
513 UnderIndentOutcome {
514 warning: LintWarning {
515 rule_name: Some("MD077".to_string()),
516 line: line.line_num,
517 column: 1,
518 end_line: warn_end_line,
519 end_column: warn_end_column,
520 message,
521 severity: Severity::Warning,
522 fix,
523 },
524 also_flag_line: compound_closer,
525 }
526 }
527}
528
529struct ContinuationLine<'a> {
533 line_num: usize,
534 info: &'a LineInfo,
535 trimmed: &'a str,
536 actual: usize,
537 saw_blank: bool,
538 saw_nested: bool,
542}
543
544struct UnderIndentOutcome {
549 warning: LintWarning,
550 also_flag_line: Option<usize>,
551}
552
553impl Rule for MD077ListContinuationIndent {
554 fn name(&self) -> &'static str {
555 "MD077"
556 }
557
558 fn description(&self) -> &'static str {
559 "List continuation content indentation"
560 }
561
562 fn check(&self, ctx: &LintContext) -> LintResult {
563 if ctx.content.is_empty() {
564 return Ok(Vec::new());
565 }
566
567 let strict_indent = ctx.flavor.requires_strict_list_indent();
568 let total_lines = ctx.lines.len();
569 let mut warnings = Vec::new();
570 let mut flagged_lines = std::collections::HashSet::new();
571
572 let mut items: Vec<(usize, usize, usize, Option<usize>)> = Vec::new();
581 for block in &ctx.list_blocks {
582 for &item_line in &block.item_lines {
583 if let Some(info) = ctx.line_info(item_line)
584 && let Some(ref li) = info.list_item
585 {
586 let line = info.content(ctx.content);
587 let task_col = Self::is_task_list_item(line, li.content_column)
588 .then_some(li.content_column + Self::TASK_CHECKBOX_PREFIX_LEN);
589 items.push((item_line, li.marker_column, li.content_column, task_col));
590 }
591 }
592 }
593 items.sort_unstable();
594 items.dedup_by_key(|&mut (ln, _, _, _)| ln);
595
596 let scoped: Vec<(usize, usize, usize, Option<usize>, usize, usize)> = items
600 .iter()
601 .enumerate()
602 .map(|(item_idx, &(item_line, marker_col, content_col, task_col))| {
603 let required = if strict_indent { content_col.max(4) } else { content_col };
604 let range_end = items
605 .iter()
606 .skip(item_idx + 1)
607 .find(|&&(_, mc, _, _)| mc <= marker_col)
608 .map_or(total_lines, |&(ln, _, _, _)| ln - 1);
609 (item_line, marker_col, content_col, task_col, required, range_end)
610 })
611 .collect();
612
613 let aligned = self.config.style == ContinuationStyle::Aligned;
639 for &(item_line, marker_col, _content_col, _task_col, required, range_end) in &scoped {
640 let has_latent_structure = aligned && Self::item_range_has_latent_structure(ctx, item_line, range_end);
655 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
656 let actual = line.actual;
657 let under_indented = actual < required;
658 let loose_escape = line.saw_blank && under_indented;
659 let confirmed_structure = line.info.in_code_block || line.info.blockquote.is_some();
666 let aligned_tight = aligned
667 && !has_latent_structure
668 && !line.saw_blank
669 && !line.saw_nested
670 && under_indented
671 && !confirmed_structure;
672 if (loose_escape || aligned_tight) && flagged_lines.insert(line.line_num) {
673 let message = if line.saw_blank {
674 if strict_indent {
675 format!(
676 "Content inside list item needs {required} spaces of indentation \
677 for MkDocs compatibility (found {actual})",
678 )
679 } else {
680 format!(
681 "Content after blank line in list item needs {required} spaces of \
682 indentation to remain part of the list (found {actual})",
683 )
684 }
685 } else {
686 format!("Continuation line under-indented (expected {required}, found {actual})")
687 };
688 let outcome = Self::build_under_indent_warning(ctx, line, required, message);
689 if let Some(closer_line) = outcome.also_flag_line {
690 flagged_lines.insert(closer_line);
691 }
692 warnings.push(outcome.warning);
693 }
694 ControlFlow::Continue(())
695 });
696 }
697
698 for &(item_line, marker_col, content_col, task_col, required, range_end) in &scoped {
707 let (uses_content_col, uses_task_col) = match task_col {
711 Some(t) => Self::sibling_column_usage(ctx, item_line, range_end, marker_col, content_col, t),
712 None => (false, false),
713 };
714
715 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
716 let actual = line.actual;
717 if actual > required
718 && !line.info.in_code_block
719 && Some(actual) != task_col
720 && !Self::starts_with_list_marker(line.trimmed)
721 && flagged_lines.insert(line.line_num)
722 {
723 let fix_target =
724 Self::compute_fix_target(actual, required, task_col, uses_content_col, uses_task_col);
725 let message = match task_col {
726 Some(t) => format!(
727 "Continuation line over-indented \
728 (expected {required} or {t}, found {actual})"
729 ),
730 None => {
731 format!("Continuation line over-indented (expected {required}, found {actual})")
732 }
733 };
734 warnings.push(Self::build_over_indent_warning(ctx, line, fix_target, message));
735 }
736 ControlFlow::Continue(())
737 });
738 }
739
740 warnings.sort_by_key(|w| (w.line, w.column));
743
744 Ok(warnings)
745 }
746
747 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
748 let warnings = self.check(ctx)?;
749 let warnings =
750 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
751 if warnings.is_empty() {
752 return Ok(ctx.content.to_string());
753 }
754
755 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
757 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
758
759 let mut content = ctx.content.to_string();
760 for fix in fixes {
761 if fix.range.start <= content.len() && fix.range.end <= content.len() {
762 content.replace_range(fix.range, &fix.replacement);
763 }
764 }
765
766 Ok(content)
767 }
768
769 fn category(&self) -> RuleCategory {
770 RuleCategory::List
771 }
772
773 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
774 ctx.content.is_empty() || ctx.list_blocks.is_empty()
775 }
776
777 fn as_any(&self) -> &dyn std::any::Any {
778 self
779 }
780
781 crate::impl_rule_config_methods!(MD077Config);
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787 use crate::config::MarkdownFlavor;
788
789 fn check(content: &str) -> Vec<LintWarning> {
790 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
791 let rule = MD077ListContinuationIndent::default();
792 rule.check(&ctx).unwrap()
793 }
794
795 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
796 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
797 let rule = MD077ListContinuationIndent::default();
798 rule.check(&ctx).unwrap()
799 }
800
801 fn fix(content: &str) -> String {
802 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
803 let rule = MD077ListContinuationIndent::default();
804 rule.fix(&ctx).unwrap()
805 }
806
807 fn fix_mkdocs(content: &str) -> String {
808 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
809 let rule = MD077ListContinuationIndent::default();
810 rule.fix(&ctx).unwrap()
811 }
812
813 fn aligned_rule() -> MD077ListContinuationIndent {
814 MD077ListContinuationIndent::new(ContinuationStyle::Aligned)
815 }
816
817 fn check_aligned(content: &str) -> Vec<LintWarning> {
818 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
819 aligned_rule().check(&ctx).unwrap()
820 }
821
822 fn check_aligned_mkdocs(content: &str) -> Vec<LintWarning> {
823 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
824 aligned_rule().check(&ctx).unwrap()
825 }
826
827 fn fix_aligned(content: &str) -> String {
828 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
829 aligned_rule().fix(&ctx).unwrap()
830 }
831
832 fn fix_aligned_quarto(content: &str) -> String {
833 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
834 aligned_rule().fix(&ctx).unwrap()
835 }
836
837 #[test]
838 fn aligned_idempotent_with_latent_marker_behind_unstable_heading() {
839 let input = "1. \n``\n``\n- \n``";
848 let once = fix_aligned_quarto(input);
849 let twice = fix_aligned_quarto(&once);
850 assert_eq!(once, twice, "MD077 aligned fix must be idempotent (Quarto)");
851 }
852
853 #[test]
856 fn tight_lazy_continuation_zero_indent_not_flagged() {
857 let content = "- Item\ncontinuation\n";
859 assert!(check(content).is_empty());
860 }
861
862 #[test]
863 fn tight_continuation_correct_indent_not_flagged() {
864 let content = "1. Item\n continuation\n";
866 assert!(check(content).is_empty());
867 }
868
869 #[test]
870 fn tight_continuation_over_indented_ordered() {
871 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
873 let warnings = check(content);
874 assert_eq!(warnings.len(), 1);
875 assert_eq!(warnings[0].line, 2);
876 assert!(warnings[0].message.contains("over-indented"));
877 }
878
879 #[test]
880 fn tight_continuation_over_indented_unordered() {
881 let content = "- Item\n over-indented\n";
883 let warnings = check(content);
884 assert_eq!(warnings.len(), 1);
885 assert_eq!(warnings[0].line, 2);
886 }
887
888 #[test]
889 fn tight_continuation_multiple_over_indented_lines() {
890 let content = "1. Item\n line one\n line two\n line three\n";
891 let warnings = check(content);
892 assert_eq!(warnings.len(), 3);
893 }
894
895 #[test]
896 fn tight_continuation_mixed_correct_and_over() {
897 let content = "1. Item\n correct\n over-indented\n correct again\n";
898 let warnings = check(content);
899 assert_eq!(warnings.len(), 1);
900 assert_eq!(warnings[0].line, 3);
901 }
902
903 #[test]
904 fn tight_continuation_nested_over_indented() {
905 let content = "- L1\n - L2\n over-indented continuation of L2\n";
907 let warnings = check(content);
908 assert_eq!(warnings.len(), 1);
909 assert_eq!(warnings[0].line, 3);
910 assert!(warnings[0].message.contains("expected 4"));
912 assert!(warnings[0].message.contains("found 5"));
913 }
914
915 #[test]
916 fn tight_continuation_nested_correct_indent_not_flagged() {
917 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
920 assert!(check(content).is_empty());
921 }
922
923 #[test]
924 fn fix_tight_continuation_nested_over_indented() {
925 let content = "- L1\n - L2\n over-indented continuation of L2\n";
927 let fixed = fix(content);
928 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
929 }
930
931 #[test]
932 fn tight_continuation_under_indented_not_flagged() {
933 let content = "1. Item\n under-indented\n";
936 assert!(check(content).is_empty());
937 }
938
939 #[test]
940 fn tight_continuation_tab_over_indented() {
941 let content = "- Item\n\tover-indented\n";
943 let warnings = check(content);
944 assert_eq!(warnings.len(), 1);
945 }
946
947 #[test]
948 fn fix_tight_continuation_over_indented_ordered() {
949 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
950 let fixed = fix(content);
951 assert_eq!(
952 fixed,
953 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
954 );
955 }
956
957 #[test]
958 fn fix_tight_continuation_over_indented_unordered() {
959 let content = "- Item\n over-indented\n";
960 let fixed = fix(content);
961 assert_eq!(fixed, "- Item\n over-indented\n");
962 }
963
964 #[test]
965 fn fix_tight_continuation_multiple_lines() {
966 let content = "1. Item\n line one\n line two\n";
967 let fixed = fix(content);
968 assert_eq!(fixed, "1. Item\n line one\n line two\n");
969 }
970
971 #[test]
972 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
973 let content = "1. Item\n continuation\n";
976 assert!(check_mkdocs(content).is_empty());
977 }
978
979 #[test]
980 fn tight_continuation_mkdocs_5space_ordered_flagged() {
981 let content = "1. Item\n over-indented\n";
983 let warnings = check_mkdocs(content);
984 assert_eq!(warnings.len(), 1);
985 assert!(warnings[0].message.contains("expected 4"));
986 assert!(warnings[0].message.contains("found 5"));
987 }
988
989 #[test]
990 fn fix_tight_continuation_mkdocs_over_indented() {
991 let content = "1. Item\n over-indented\n";
992 let fixed = fix_mkdocs(content);
993 assert_eq!(fixed, "1. Item\n over-indented\n");
994 }
995
996 #[test]
997 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
998 let content = "* Level 0\n * Level 1\n * Level 2\n";
1001 assert!(check(content).is_empty());
1002 }
1003
1004 #[test]
1005 fn tight_continuation_ordered_marker_not_flagged() {
1006 let content = "- Parent\n 1. Child item\n";
1008 assert!(check(content).is_empty());
1009 }
1010
1011 #[test]
1014 fn unordered_correct_indent_no_warning() {
1015 let content = "- Item\n\n continuation\n";
1016 assert!(check(content).is_empty());
1017 }
1018
1019 #[test]
1020 fn unordered_partial_indent_warns() {
1021 let content = "- Item\n\n continuation\n";
1024 let warnings = check(content);
1025 assert_eq!(warnings.len(), 1);
1026 assert_eq!(warnings[0].line, 3);
1027 assert!(warnings[0].message.contains("2 spaces"));
1028 assert!(warnings[0].message.contains("found 1"));
1029 }
1030
1031 #[test]
1032 fn unordered_zero_indent_is_new_paragraph() {
1033 let content = "- Item\n\ncontinuation\n";
1036 assert!(check(content).is_empty());
1037 }
1038
1039 #[test]
1042 fn ordered_3space_correct_commonmark() {
1043 let content = "1. Item\n\n continuation\n";
1045 assert!(check(content).is_empty());
1046 }
1047
1048 #[test]
1049 fn ordered_2space_under_indent_commonmark() {
1050 let content = "1. Item\n\n continuation\n";
1051 let warnings = check(content);
1052 assert_eq!(warnings.len(), 1);
1053 assert!(warnings[0].message.contains("3 spaces"));
1054 assert!(warnings[0].message.contains("found 2"));
1055 }
1056
1057 #[test]
1060 fn multi_digit_marker_correct() {
1061 let content = "10. Item\n\n continuation\n";
1063 assert!(check(content).is_empty());
1064 }
1065
1066 #[test]
1067 fn multi_digit_marker_under_indent() {
1068 let content = "10. Item\n\n continuation\n";
1069 let warnings = check(content);
1070 assert_eq!(warnings.len(), 1);
1071 assert!(warnings[0].message.contains("4 spaces"));
1072 }
1073
1074 #[test]
1077 fn mkdocs_3space_ordered_warns() {
1078 let content = "1. Item\n\n continuation\n";
1080 let warnings = check_mkdocs(content);
1081 assert_eq!(warnings.len(), 1);
1082 assert!(warnings[0].message.contains("4 spaces"));
1083 assert!(warnings[0].message.contains("MkDocs"));
1084 }
1085
1086 #[test]
1087 fn mkdocs_4space_ordered_no_warning() {
1088 let content = "1. Item\n\n continuation\n";
1089 assert!(check_mkdocs(content).is_empty());
1090 }
1091
1092 #[test]
1093 fn mkdocs_unordered_2space_ok() {
1094 let content = "- Item\n\n continuation\n";
1096 assert!(check_mkdocs(content).is_empty());
1097 }
1098
1099 #[test]
1100 fn mkdocs_unordered_2space_warns() {
1101 let content = "- Item\n\n continuation\n";
1103 let warnings = check_mkdocs(content);
1104 assert_eq!(warnings.len(), 1);
1105 assert!(warnings[0].message.contains("4 spaces"));
1106 }
1107
1108 #[test]
1111 fn fix_unordered_indent() {
1112 let content = "- Item\n\n continuation\n";
1114 let fixed = fix(content);
1115 assert_eq!(fixed, "- Item\n\n continuation\n");
1116 }
1117
1118 #[test]
1119 fn fix_ordered_indent() {
1120 let content = "1. Item\n\n continuation\n";
1121 let fixed = fix(content);
1122 assert_eq!(fixed, "1. Item\n\n continuation\n");
1123 }
1124
1125 #[test]
1126 fn fix_mkdocs_indent() {
1127 let content = "1. Item\n\n continuation\n";
1128 let fixed = fix_mkdocs(content);
1129 assert_eq!(fixed, "1. Item\n\n continuation\n");
1130 }
1131
1132 #[test]
1135 fn nested_list_items_not_flagged() {
1136 let content = "- Parent\n\n - Child\n";
1137 assert!(check(content).is_empty());
1138 }
1139
1140 #[test]
1141 fn nested_list_zero_indent_is_new_paragraph() {
1142 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
1144 assert!(check(content).is_empty());
1145 }
1146
1147 #[test]
1148 fn nested_list_partial_indent_flagged() {
1149 let content = "- Parent\n - Child\n\n continuation of parent\n";
1151 let warnings = check(content);
1152 assert_eq!(warnings.len(), 1);
1153 assert!(warnings[0].message.contains("2 spaces"));
1154 }
1155
1156 #[test]
1159 fn code_block_correctly_indented_no_warning() {
1160 let content = "- Item\n\n ```\n code\n ```\n";
1162 assert!(check(content).is_empty());
1163 }
1164
1165 #[test]
1166 fn code_fence_under_indented_warns() {
1167 let content = "- Item\n\n ```\n code\n ```\n";
1171 let warnings = check(content);
1172 assert_eq!(warnings.len(), 1);
1173 assert_eq!(warnings[0].line, 3);
1174 }
1175
1176 #[test]
1177 fn code_fence_under_indented_ordered_mkdocs() {
1178 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
1181 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
1183 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1185 assert!(warnings[0].message.contains("4 spaces"));
1186 assert!(warnings[0].message.contains("MkDocs"));
1187 }
1188
1189 #[test]
1190 fn code_fence_tilde_under_indented() {
1191 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1192 let warnings = check(content);
1193 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1195 }
1196
1197 #[test]
1200 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
1201 let content = "- Item\n\n\ncontinuation\n";
1203 assert!(check(content).is_empty());
1204 }
1205
1206 #[test]
1207 fn multiple_blank_lines_partial_indent_flags() {
1208 let content = "- Item\n\n\n continuation\n";
1209 let warnings = check(content);
1210 assert_eq!(warnings.len(), 1);
1211 }
1212
1213 #[test]
1216 fn empty_item_no_warning() {
1217 let content = "- \n- Second\n";
1218 assert!(check(content).is_empty());
1219 }
1220
1221 #[test]
1224 fn multiple_items_mixed_indent() {
1225 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
1226 let warnings = check(content);
1227 assert_eq!(warnings.len(), 1);
1228 assert_eq!(warnings[0].line, 7);
1229 }
1230
1231 #[test]
1234 fn task_list_correct_indent() {
1235 let content = "- [ ] Task\n\n continuation\n";
1237 assert!(check(content).is_empty());
1238 }
1239
1240 #[test]
1243 fn frontmatter_not_flagged() {
1244 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
1245 assert!(check(content).is_empty());
1246 }
1247
1248 #[test]
1251 fn fix_multiple_items() {
1252 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
1253 let fixed = fix(content);
1254 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
1255 }
1256
1257 #[test]
1258 fn fix_multiline_loose_continuation_all_lines() {
1259 let content = "1. Item\n\n line one\n line two\n line three\n";
1260 let fixed = fix(content);
1261 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
1262 }
1263
1264 #[test]
1267 fn sibling_item_boundary_respected() {
1268 let content = "- First\n- Second\n\n continuation\n";
1270 assert!(check(content).is_empty());
1271 }
1272
1273 #[test]
1276 fn blockquote_list_correct_indent_no_warning() {
1277 let content = "> - Item\n>\n> continuation\n";
1280 assert!(check(content).is_empty());
1281 }
1282
1283 #[test]
1284 fn blockquote_list_under_indent_no_false_positive() {
1285 let content = "> - Item\n>\n> continuation\n";
1290 assert!(check(content).is_empty());
1291 }
1292
1293 #[test]
1296 fn deeply_nested_correct_indent() {
1297 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1298 assert!(check(content).is_empty());
1299 }
1300
1301 #[test]
1302 fn deeply_nested_under_indent() {
1303 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1306 let warnings = check(content);
1307 assert_eq!(warnings.len(), 1);
1308 assert!(warnings[0].message.contains("6 spaces"));
1309 assert!(warnings[0].message.contains("found 5"));
1310 }
1311
1312 #[test]
1315 fn loose_tab_continuation_over_indented() {
1316 let content = "- Item\n\n\tcontinuation\n";
1321 let warnings = check(content);
1322 assert_eq!(warnings.len(), 1);
1323 assert_eq!(warnings[0].line, 3);
1324 assert_eq!(fix(content), "- Item\n\n continuation\n");
1325 }
1326
1327 #[test]
1330 fn multiple_continuations_correct() {
1331 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
1332 assert!(check(content).is_empty());
1333 }
1334
1335 #[test]
1336 fn multiple_continuations_second_under_indent() {
1337 let content = "- Item\n\n para 1\n\n continuation 2\n";
1339 let warnings = check(content);
1340 assert_eq!(warnings.len(), 1);
1341 assert_eq!(warnings[0].line, 5);
1342 }
1343
1344 #[test]
1347 fn ordered_paren_marker_correct() {
1348 let content = "1) Item\n\n continuation\n";
1350 assert!(check(content).is_empty());
1351 }
1352
1353 #[test]
1354 fn ordered_paren_marker_under_indent() {
1355 let content = "1) Item\n\n continuation\n";
1356 let warnings = check(content);
1357 assert_eq!(warnings.len(), 1);
1358 assert!(warnings[0].message.contains("3 spaces"));
1359 }
1360
1361 #[test]
1364 fn star_marker_correct() {
1365 let content = "* Item\n\n continuation\n";
1366 assert!(check(content).is_empty());
1367 }
1368
1369 #[test]
1370 fn star_marker_under_indent() {
1371 let content = "* Item\n\n continuation\n";
1372 let warnings = check(content);
1373 assert_eq!(warnings.len(), 1);
1374 }
1375
1376 #[test]
1377 fn plus_marker_correct() {
1378 let content = "+ Item\n\n continuation\n";
1379 assert!(check(content).is_empty());
1380 }
1381
1382 #[test]
1385 fn heading_after_list_no_warning() {
1386 let content = "- Item\n\n# Heading\n";
1387 assert!(check(content).is_empty());
1388 }
1389
1390 #[test]
1393 fn hr_after_list_no_warning() {
1394 let content = "- Item\n\n---\n";
1395 assert!(check(content).is_empty());
1396 }
1397
1398 #[test]
1401 fn reference_link_def_not_flagged() {
1402 let content = "- Item\n\n [link]: https://example.com\n";
1403 assert!(check(content).is_empty());
1404 }
1405
1406 #[test]
1409 fn footnote_def_not_flagged() {
1410 let content = "- Item\n\n [^1]: footnote text\n";
1411 assert!(check(content).is_empty());
1412 }
1413
1414 #[test]
1415 fn footnote_multiline_body_after_list_not_flagged() {
1416 let content = "# A list followed by a footnote\n\n\
1420 Here is a paragraph.[^fn]\n\n\
1421 - This is a list.\n\n\
1422 [^fn]:\n\
1423 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1424 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1425 assert!(check(content).is_empty());
1426 }
1427
1428 #[test]
1429 fn fix_footnote_multiline_body_after_list_is_noop() {
1430 let content = "# A list followed by a footnote\n\n\
1434 Here is a paragraph.[^fn]\n\n\
1435 - This is a list.\n\n\
1436 [^fn]:\n\
1437 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1438 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1439 assert_eq!(fix(content), content);
1440 }
1441
1442 #[test]
1443 fn footnote_body_indented_past_list_content_col_not_flagged() {
1444 let content = "- Item\n\n[^fn]:\n Body line one.\n Body line two.\n";
1448 assert!(check(content).is_empty());
1449 }
1450
1451 #[test]
1452 fn list_inside_footnote_body_continuation_not_flagged() {
1453 let content = "Text.[^fn]\n\n[^fn]:\n\
1457 \x20\x20\x20\x20- nested item\n\
1458 \x20\x20\x20\x20\x20\x20\x20over-indented continuation\n";
1459 assert!(check(content).is_empty());
1460 }
1461
1462 #[test]
1463 fn footnote_multiline_body_after_list_not_flagged_mkdocs() {
1464 let content = "Here is a paragraph.[^fn]\n\n\
1468 - This is a list.\n\n\
1469 [^fn]:\n\
1470 \x20\x20\x20\x20\x20\x20Footnote body that spans\n\
1471 \x20\x20\x20\x20\x20\x20multiple indented lines.\n";
1472 assert!(check_mkdocs(content).is_empty());
1473 }
1474
1475 #[test]
1478 fn fix_deeply_nested() {
1479 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
1480 let fixed = fix(content);
1481 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
1482 }
1483
1484 #[test]
1485 fn fix_mkdocs_unordered() {
1486 let content = "- Item\n\n continuation\n";
1488 let fixed = fix_mkdocs(content);
1489 assert_eq!(fixed, "- Item\n\n continuation\n");
1490 }
1491
1492 #[test]
1493 fn fix_code_fence_indent() {
1494 let content = "- Item\n\n ```\n code\n ```\n";
1497 let fixed = fix(content);
1498 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
1499 }
1500
1501 #[test]
1502 fn fix_mkdocs_code_fence_indent() {
1503 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
1505 let fixed = fix_mkdocs(content);
1506 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
1507 }
1508
1509 #[test]
1512 fn empty_document_no_warning() {
1513 assert!(check("").is_empty());
1514 }
1515
1516 #[test]
1517 fn whitespace_only_no_warning() {
1518 assert!(check(" \n\n \n").is_empty());
1519 }
1520
1521 #[test]
1524 fn no_list_no_warning() {
1525 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
1526 assert!(check(content).is_empty());
1527 }
1528
1529 #[test]
1532 fn multiline_continuation_all_lines_flagged() {
1533 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";
1534 let warnings = check(content);
1535 assert_eq!(warnings.len(), 3);
1536 assert_eq!(warnings[0].line, 3);
1537 assert_eq!(warnings[1].line, 4);
1538 assert_eq!(warnings[2].line, 5);
1539 }
1540
1541 #[test]
1542 fn multiline_continuation_with_frontmatter_fix() {
1543 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";
1544 let fixed = fix(content);
1545 assert_eq!(
1546 fixed,
1547 "---\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"
1548 );
1549 }
1550
1551 #[test]
1552 fn multiline_continuation_correct_indent_no_warning() {
1553 let content = "1. Item\n\n line one\n line two\n line three\n";
1554 assert!(check(content).is_empty());
1555 }
1556
1557 #[test]
1558 fn multiline_continuation_mixed_indent() {
1559 let content = "1. Item\n\n correct\n wrong\n correct\n";
1560 let warnings = check(content);
1561 assert_eq!(warnings.len(), 1);
1562 assert_eq!(warnings[0].line, 4);
1563 }
1564
1565 #[test]
1566 fn multiline_continuation_unordered() {
1567 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
1568 let warnings = check(content);
1569 assert_eq!(warnings.len(), 3);
1570 let fixed = fix(content);
1571 assert_eq!(
1572 fixed,
1573 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
1574 );
1575 }
1576
1577 #[test]
1578 fn multiline_continuation_two_items_fix() {
1579 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
1580 let fixed = fix(content);
1581 assert_eq!(
1582 fixed,
1583 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1584 );
1585 }
1586
1587 #[test]
1588 fn fence_fix_does_not_break_pairing_for_md031() {
1589 let content = "#### title\n\nabc\n\n\
1596 1. ab\n\n\
1597 \x20\x20`aabbccdd`\n\n\
1598 2. cd\n\n\
1599 \x20\x20`bbcc dd ee`\n\n\
1600 \x20\x20```\n\
1601 \x20\x20abcd\n\
1602 \x20\x20ef gh\n\
1603 \x20\x20```\n\n\
1604 \x20\x20uu\n\n\
1605 \x20\x20```\n\
1606 \x20\x20cdef\n\
1607 \x20\x20gh ij\n\
1608 \x20\x20```\n";
1609 let expected = "#### title\n\nabc\n\n\
1610 1. ab\n\n\
1611 \x20\x20\x20`aabbccdd`\n\n\
1612 2. cd\n\n\
1613 \x20\x20\x20`bbcc dd ee`\n\n\
1614 \x20\x20\x20```\n\
1615 \x20\x20\x20abcd\n\
1616 \x20\x20\x20ef gh\n\
1617 \x20\x20\x20```\n\n\
1618 \x20\x20\x20uu\n\n\
1619 \x20\x20\x20```\n\
1620 \x20\x20\x20cdef\n\
1621 \x20\x20\x20gh ij\n\
1622 \x20\x20\x20```\n";
1623 assert_eq!(fix(content), expected);
1624 }
1625
1626 #[test]
1627 fn multiline_continuation_separated_by_blank() {
1628 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1629 let warnings = check(content);
1630 assert_eq!(warnings.len(), 4);
1631 let fixed = fix(content);
1632 assert_eq!(
1633 fixed,
1634 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1635 );
1636 }
1637
1638 #[test]
1639 fn tab_indented_fence_is_normalized_to_spaces() {
1640 let content = "100. ab\n\n\t```\n\tabcd\n\t```\n";
1648 let expected = "100. ab\n\n ```\n abcd\n ```\n";
1649 assert_eq!(fix(content), expected);
1650 }
1651
1652 #[test]
1661 fn loose_continuation_over_indented_flagged() {
1662 let content = "* Item\n\n over-indented\n";
1665 let warnings = check(content);
1666 assert_eq!(warnings.len(), 1);
1667 assert_eq!(warnings[0].line, 3);
1668 assert!(warnings[0].message.contains("over-indented"));
1669 assert!(warnings[0].message.contains("expected 2"));
1670 assert!(warnings[0].message.contains("found 3"));
1671 }
1672
1673 #[test]
1674 fn loose_continuation_over_indented_multiline_mixed() {
1675 let content = "* Item\n\n over one\n correct\n over two\n";
1677 let warnings = check(content);
1678 assert_eq!(warnings.len(), 2);
1679 assert_eq!(warnings[0].line, 3);
1680 assert_eq!(warnings[1].line, 5);
1681 }
1682
1683 #[test]
1684 fn fix_loose_continuation_over_indented() {
1685 let content = "* Item\n\n over one\n correct\n over two\n";
1686 let fixed = fix(content);
1687 assert_eq!(fixed, "* Item\n\n over one\n correct\n over two\n");
1688 }
1689
1690 #[test]
1691 fn fix_tight_and_loose_items_normalized_identically() {
1692 let content = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1695 * 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\
1696 * 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";
1697 let expected = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1698 * 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\
1699 * 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";
1700 assert_eq!(fix(content), expected);
1701 }
1702
1703 #[test]
1704 fn multi_paragraph_item_loose_paragraph_over_indented() {
1705 let content = "* Item.\n tight over\n\n loose over\n";
1708 let warnings = check(content);
1709 assert_eq!(warnings.len(), 2);
1710 assert_eq!(warnings[0].line, 2);
1711 assert_eq!(warnings[1].line, 4);
1712 }
1713
1714 #[test]
1715 fn loose_indented_code_block_not_flagged() {
1716 let content = "- Item\n\n code line\n";
1720 assert!(check(content).is_empty());
1721 }
1722
1723 #[test]
1724 fn mkdocs_loose_over_indented_flagged() {
1725 let content = "1. Item\n\n over\n";
1728 let warnings = check_mkdocs(content);
1729 assert_eq!(warnings.len(), 1);
1730 assert_eq!(warnings[0].line, 3);
1731 assert!(warnings[0].message.contains("over-indented"));
1732 assert!(warnings[0].message.contains("expected 4"));
1733 assert!(warnings[0].message.contains("found 5"));
1734 }
1735
1736 #[test]
1737 fn task_list_loose_over_indented_flagged() {
1738 let content = "- [ ] Task\n\n over\n";
1741 let warnings = check(content);
1742 assert_eq!(warnings.len(), 1);
1743 assert_eq!(warnings[0].line, 3);
1744 }
1745
1746 #[test]
1747 fn loose_over_indent_boundary_below_code_block_threshold_flagged() {
1748 let content = "- Item\n\n over\n";
1753 let warnings = check(content);
1754 assert_eq!(warnings.len(), 1);
1755 assert_eq!(warnings[0].line, 3);
1756 assert!(warnings[0].message.contains("expected 2"));
1757 assert!(warnings[0].message.contains("found 5"));
1758 }
1759
1760 #[test]
1761 fn loose_over_indent_does_not_steal_nested_under_indent() {
1762 let content = "- Outer\n - Inner\n\n continuation\n";
1769 let warnings = check(content);
1770 assert_eq!(warnings.len(), 1);
1771 assert_eq!(warnings[0].line, 4);
1772 assert!(warnings[0].message.contains("4 spaces"));
1773 assert!(warnings[0].message.contains("found 3"));
1774 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1775 }
1776
1777 #[test]
1778 fn loose_over_indent_attributes_to_deepest_enclosing_item() {
1779 let content = "- Outer\n - Inner\n\n continuation\n";
1783 let warnings = check(content);
1784 assert_eq!(warnings.len(), 1);
1785 assert_eq!(warnings[0].line, 4);
1786 assert!(warnings[0].message.contains("expected 4"));
1787 assert!(warnings[0].message.contains("found 5"));
1788 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1789 }
1790
1791 #[test]
1800 fn loose_over_indented_fence_not_flagged() {
1801 let content = "- Item\n\n ```\n code\n ```\n";
1802 assert!(check(content).is_empty());
1803 assert_eq!(fix(content), content);
1804 }
1805
1806 #[test]
1807 fn tight_over_indented_fence_not_flagged() {
1808 let content = "- Item\n ```\n code\n ```\n";
1809 assert!(check(content).is_empty());
1810 assert_eq!(fix(content), content);
1811 }
1812
1813 #[test]
1814 fn over_indented_tilde_fence_not_flagged() {
1815 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1816 assert!(check(content).is_empty());
1817 assert_eq!(fix(content), content);
1818 }
1819
1820 #[test]
1821 fn fence_like_code_content_inside_fenced_block_not_flagged() {
1822 let content = "- Item\n\n ~~~\n ```\n ~~~\n";
1825 assert!(check(content).is_empty());
1826 assert_eq!(fix(content), content);
1827 }
1828
1829 #[test]
1830 fn unterminated_over_indented_fence_not_flagged() {
1831 let content = "- Item\n\n ```\n code1\n code2deeper\n";
1834 assert!(check(content).is_empty());
1835 assert_eq!(fix(content), content);
1836 }
1837
1838 #[test]
1846 fn task_list_tight_continuation_post_checkbox_reproducer_579() {
1847 let content = "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n";
1850 assert!(check(content).is_empty());
1851 }
1852
1853 #[test]
1854 fn task_list_tight_continuation_dash_unchecked() {
1855 let content = "- [ ] Task\n continuation\n";
1856 assert!(check(content).is_empty());
1857 }
1858
1859 #[test]
1860 fn task_list_tight_continuation_dash_checked_lower() {
1861 let content = "- [x] Task\n continuation\n";
1862 assert!(check(content).is_empty());
1863 }
1864
1865 #[test]
1866 fn task_list_tight_continuation_dash_checked_upper() {
1867 let content = "- [X] Task\n continuation\n";
1868 assert!(check(content).is_empty());
1869 }
1870
1871 #[test]
1872 fn task_list_tight_continuation_star_marker() {
1873 let content = "* [ ] Task\n continuation\n";
1874 assert!(check(content).is_empty());
1875 }
1876
1877 #[test]
1878 fn task_list_tight_continuation_plus_marker() {
1879 let content = "+ [ ] Task\n continuation\n";
1880 assert!(check(content).is_empty());
1881 }
1882
1883 #[test]
1884 fn task_list_tight_continuation_content_column_still_valid() {
1885 let content = "- [ ] Task\n continuation\n";
1888 assert!(check(content).is_empty());
1889 }
1890
1891 #[test]
1892 fn task_list_tight_continuation_between_columns_still_flagged() {
1893 let content = "- [ ] Task\n continuation\n";
1896 let warnings = check(content);
1897 assert_eq!(warnings.len(), 1);
1898 assert!(warnings[0].message.contains("expected 2 or 6"));
1900 assert!(warnings[0].message.contains("found 4"));
1901 }
1902
1903 #[test]
1904 fn task_list_tight_continuation_overshoot_still_flagged() {
1905 let content = "- [ ] Task\n continuation\n";
1907 let warnings = check(content);
1908 assert_eq!(warnings.len(), 1);
1909 assert!(warnings[0].message.contains("expected 2 or 6"));
1910 assert!(warnings[0].message.contains("found 7"));
1911 }
1912
1913 #[test]
1916 fn fix_task_list_overshoot_snaps_to_task_col() {
1917 let content = "- [ ] Task\n continuation\n";
1921 let fixed = fix(content);
1922 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1923 }
1924
1925 #[test]
1926 fn fix_task_list_col_5_snaps_to_task_col() {
1927 let content = "- [ ] Task\n continuation\n";
1929 let fixed = fix(content);
1930 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1931 }
1932
1933 #[test]
1934 fn fix_task_list_col_3_snaps_to_content_col() {
1935 let content = "- [ ] Task\n continuation\n";
1937 let fixed = fix(content);
1938 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1939 }
1940
1941 #[test]
1942 fn fix_task_list_col_4_ties_to_content_col() {
1943 let content = "- [ ] Task\n continuation\n";
1948 let fixed = fix(content);
1949 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1950 }
1951
1952 #[test]
1953 fn fix_task_list_ordered_overshoot_snaps_to_task_col() {
1954 let content = "1. [ ] Task\n continuation\n";
1957 let fixed = fix(content);
1958 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1959 }
1960
1961 #[test]
1962 fn fix_task_list_ordered_under_overshoot_snaps_to_content_col() {
1963 let content = "1. [ ] Task\n continuation\n";
1966 let fixed = fix(content);
1967 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
1968 }
1969
1970 #[test]
1971 fn task_list_tight_continuation_ordered_single_digit() {
1972 let content = "1. [ ] Task\n continuation\n";
1974 assert!(check(content).is_empty());
1975 }
1976
1977 #[test]
1978 fn task_list_tight_continuation_ordered_multi_digit() {
1979 let content = "10. [ ] Task\n continuation\n";
1981 assert!(check(content).is_empty());
1982 }
1983
1984 #[test]
1985 fn task_list_tight_continuation_nested_dash() {
1986 let content = "- Parent\n - [ ] Nested task\n continuation\n";
1988 assert!(check(content).is_empty());
1989 }
1990
1991 #[test]
1992 fn task_list_loose_continuation_post_checkbox_column_not_flagged() {
1993 let content = "- [ ] Task\n\n continuation\n";
1998 assert!(check(content).is_empty());
1999 }
2000
2001 #[test]
2002 fn task_list_empty_body_is_not_a_task() {
2003 let content = "- [ ]\n continuation\n";
2009 let warnings = check(content);
2010 assert_eq!(warnings.len(), 1);
2011 assert!(warnings[0].message.contains("found 4"));
2012 }
2013
2014 #[test]
2015 fn task_list_malformed_checkbox_is_not_a_task() {
2016 let content = "- [~] Not a task\n continuation\n";
2018 let warnings = check(content);
2019 assert_eq!(warnings.len(), 1);
2020 }
2021
2022 #[test]
2029 fn task_list_mkdocs_unordered_required_min_valid() {
2030 let content = "- [ ] Task\n continuation\n";
2032 assert!(check_mkdocs(content).is_empty());
2033 }
2034
2035 #[test]
2036 fn task_list_mkdocs_unordered_post_checkbox_valid() {
2037 let content = "- [ ] Task\n continuation\n";
2038 assert!(check_mkdocs(content).is_empty());
2039 }
2040
2041 #[test]
2042 fn task_list_mkdocs_unordered_between_flagged() {
2043 let content = "- [ ] Task\n continuation\n";
2045 let warnings = check_mkdocs(content);
2046 assert_eq!(warnings.len(), 1);
2047 }
2048
2049 #[test]
2050 fn task_list_mkdocs_ordered_both_columns_valid() {
2051 let at_4 = "1. [ ] Task\n continuation\n";
2053 assert!(check_mkdocs(at_4).is_empty());
2054 let at_7 = "1. [ ] Task\n continuation\n";
2055 assert!(check_mkdocs(at_7).is_empty());
2056 }
2057
2058 #[test]
2059 fn task_list_mkdocs_ordered_between_flagged() {
2060 let at_5 = "1. [ ] Task\n continuation\n";
2062 assert_eq!(check_mkdocs(at_5).len(), 1);
2063 let at_6 = "1. [ ] Task\n continuation\n";
2064 assert_eq!(check_mkdocs(at_6).len(), 1);
2065 }
2066
2067 #[test]
2077 fn fix_task_list_tie_sibling_at_task_col_snaps_to_task_col() {
2078 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
2082 let fixed = fix(content);
2083 assert_eq!(
2084 fixed,
2085 "- [ ] Task\n aligned continuation\n tied continuation\n"
2086 );
2087 }
2088
2089 #[test]
2090 fn fix_task_list_tie_sibling_at_content_col_snaps_to_content_col() {
2091 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
2094 let fixed = fix(content);
2095 assert_eq!(fixed, "- [ ] Task\n aligned continuation\n tied continuation\n");
2096 }
2097
2098 #[test]
2099 fn fix_task_list_tie_both_siblings_snaps_to_content_col() {
2100 let content = "- [ ] Task\n at content col\n at task col\n tied continuation\n";
2104 let fixed = fix(content);
2105 assert_eq!(
2106 fixed,
2107 "- [ ] Task\n at content col\n at task col\n tied continuation\n"
2108 );
2109 }
2110
2111 #[test]
2112 fn fix_task_list_tie_sees_task_col_through_tight_lazy_continuation() {
2113 let content = concat!("- [ ] Task\n", "lazy\n", " aligned at task col\n", " tied\n",);
2127 let fixed = fix(content);
2128 assert!(
2129 fixed.contains("\n tied\n"),
2130 "tied line should snap to col 6 (task col) because a task-col \
2131 sibling is visible past the tight lazy-continuation line; got:\n{fixed}"
2132 );
2133 }
2134
2135 #[test]
2142 fn task_list_tab_indented_continuation_flagged() {
2143 let content = "- [ ] Task\n\t\twrap\n";
2146 let warnings = check(content);
2147 assert_eq!(warnings.len(), 1);
2148 assert!(warnings[0].message.contains("expected 2 or 6"));
2149 assert!(warnings[0].message.contains("found 8"));
2150 }
2151
2152 #[test]
2153 fn fix_task_list_tab_indented_snaps_to_task_col() {
2154 let content = "- [ ] Task\n\t\twrap\n";
2156 let fixed = fix(content);
2157 assert_eq!(fixed, "- [ ] Task\n wrap\n");
2158 }
2159
2160 #[test]
2161 fn fix_task_list_single_tab_equidistant_snaps_to_content_col() {
2162 let content = "- [ ] Task\n\twrap\n";
2165 let fixed = fix(content);
2166 assert_eq!(fixed, "- [ ] Task\n wrap\n");
2167 }
2168
2169 #[test]
2179 fn task_list_blockquote_post_checkbox_not_flagged() {
2180 let content = "> - [ ] Task\n> continuation\n";
2182 assert!(check(content).is_empty());
2183 }
2184
2185 #[test]
2186 fn task_list_blockquote_between_cols_documented_limitation() {
2187 let content = "> - [ ] Task\n> continuation\n";
2191 assert!(check(content).is_empty());
2192 }
2193
2194 #[test]
2195 fn task_list_blockquote_overshoot_documented_limitation() {
2196 let content = "> - [ ] Task\n> continuation\n";
2198 assert!(check(content).is_empty());
2199 }
2200
2201 #[test]
2208 fn fix_task_list_mkdocs_unordered_overshoot_snaps_to_task_col() {
2209 let content = "- [ ] Task\n continuation\n";
2212 let fixed = fix_mkdocs(content);
2213 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2214 }
2215
2216 #[test]
2217 fn fix_task_list_mkdocs_unordered_tie_snaps_to_required() {
2218 let content = "- [ ] Task\n continuation\n";
2221 let fixed = fix_mkdocs(content);
2222 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2223 }
2224
2225 #[test]
2226 fn fix_task_list_mkdocs_ordered_overshoot_snaps_to_task_col() {
2227 let content = "1. [ ] Task\n continuation\n";
2230 let fixed = fix_mkdocs(content);
2231 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2232 }
2233
2234 #[test]
2235 fn fix_task_list_mkdocs_ordered_near_required_snaps_to_required() {
2236 let content = "1. [ ] Task\n continuation\n";
2242 let fixed = fix_mkdocs(content);
2243 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2244 }
2245
2246 #[test]
2247 fn fix_task_list_mkdocs_ordered_between_cols_snaps_to_task_col() {
2248 let content = "1. [ ] Task\n continuation\n";
2251 let fixed = fix_mkdocs(content);
2252 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2253 }
2254
2255 fn assert_idempotent(content: &str) {
2265 let once = fix(content);
2266 let twice = fix(&once);
2267 assert_eq!(once, twice, "MD077 fix was not idempotent on input: {content:?}");
2268 }
2269
2270 fn assert_idempotent_mkdocs(content: &str) {
2271 let once = fix_mkdocs(content);
2272 let twice = fix_mkdocs(&once);
2273 assert_eq!(
2274 once, twice,
2275 "MD077 (MkDocs) fix was not idempotent on input: {content:?}"
2276 );
2277 }
2278
2279 #[test]
2280 fn idempotent_task_list_between_cols() {
2281 assert_idempotent("- [ ] Task\n continuation\n");
2282 }
2283
2284 #[test]
2285 fn idempotent_task_list_overshoot() {
2286 assert_idempotent("- [ ] Task\n continuation\n");
2287 }
2288
2289 #[test]
2290 fn idempotent_task_list_under_post_checkbox() {
2291 assert_idempotent("- [ ] Task\n continuation\n");
2292 }
2293
2294 #[test]
2295 fn idempotent_task_list_near_post_checkbox() {
2296 assert_idempotent("- [ ] Task\n continuation\n");
2297 }
2298
2299 #[test]
2300 fn idempotent_task_list_tab_overshoot() {
2301 assert_idempotent("- [ ] Task\n\t\twrap\n");
2302 }
2303
2304 #[test]
2305 fn idempotent_task_list_single_tab() {
2306 assert_idempotent("- [ ] Task\n\twrap\n");
2307 }
2308
2309 #[test]
2310 fn idempotent_task_list_ordered_overshoot() {
2311 assert_idempotent("1. [ ] Task\n continuation\n");
2312 }
2313
2314 #[test]
2315 fn idempotent_task_list_ordered_under() {
2316 assert_idempotent("1. [ ] Task\n continuation\n");
2317 }
2318
2319 #[test]
2320 fn idempotent_task_list_tie_with_sibling_at_task_col() {
2321 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2322 }
2323
2324 #[test]
2325 fn idempotent_task_list_tie_with_sibling_at_content_col() {
2326 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2327 }
2328
2329 #[test]
2330 fn idempotent_task_list_mkdocs_unordered_overshoot() {
2331 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2332 }
2333
2334 #[test]
2335 fn idempotent_task_list_mkdocs_unordered_tie() {
2336 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2337 }
2338
2339 #[test]
2340 fn idempotent_task_list_mkdocs_ordered_overshoot() {
2341 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2342 }
2343
2344 #[test]
2345 fn idempotent_task_list_mkdocs_ordered_between() {
2346 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2347 }
2348
2349 #[test]
2350 fn idempotent_task_list_reproducer_579() {
2351 assert_idempotent(
2355 "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n",
2356 );
2357 }
2358
2359 #[test]
2360 fn idempotent_non_task_list_still_holds() {
2361 assert_idempotent("1. Item\n over-indented\n");
2364 assert_idempotent("- Item\n\n continuation\n");
2365 }
2366
2367 #[test]
2374 fn idempotent_non_task_loose_under_indent_ordered() {
2375 assert_idempotent("1. Item\n\n continuation\n");
2377 }
2378
2379 #[test]
2380 fn idempotent_non_task_loose_under_indent_multi_digit() {
2381 assert_idempotent("10. Item\n\n continuation\n");
2383 }
2384
2385 #[test]
2386 fn idempotent_non_task_tight_over_indent_ordered() {
2387 assert_idempotent("1. Item\n over-indented\n");
2389 }
2390
2391 #[test]
2399 fn idempotent_non_task_fence_ordered_loose() {
2400 assert_idempotent("1. Item\n\n ```rust\n let x = 1;\n ```\n");
2402 }
2403
2404 #[test]
2405 fn idempotent_non_task_fence_tilde_under_indent() {
2406 assert_idempotent("1. Item\n\n ~~~\nplain text\n ~~~\n");
2412 }
2413
2414 #[test]
2415 fn idempotent_non_task_fence_interior_above_required() {
2416 assert_idempotent("1. Item\n\n ```\n deeply indented code\n ```\n");
2420 }
2421
2422 #[test]
2423 fn fence_fix_promotes_interior_below_scope_in_single_pass() {
2424 let content = "1. Item\n\n ```\ncode\n ```\n";
2428 let fixed = fix(content);
2429 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2430 }
2431
2432 #[test]
2433 fn fence_fix_preserves_interior_above_required() {
2434 let content = "1. Item\n\n ```\n code\n ```\n";
2437 let fixed = fix(content);
2438 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2439 }
2440
2441 #[test]
2448 fn idempotent_non_task_mkdocs_ordered_at_3_spaces() {
2449 assert_idempotent_mkdocs("1. Item\n\n continuation\n");
2451 }
2452
2453 #[test]
2454 fn idempotent_non_task_mkdocs_unordered_at_2_spaces() {
2455 assert_idempotent_mkdocs("- Item\n\n continuation\n");
2457 }
2458
2459 #[test]
2460 fn idempotent_non_task_mkdocs_fence_compound() {
2461 assert_idempotent_mkdocs("1. Item\n\n ```toml\n k = 1\n ```\n");
2463 }
2464
2465 #[test]
2468 fn aligned_tight_zero_indent_continuation_flagged() {
2469 let content = "- this is a long line\nthat continues on a second line\n";
2473 let warnings = check_aligned(content);
2474 assert_eq!(warnings.len(), 1);
2475 assert_eq!(warnings[0].line, 2);
2476 assert_eq!(
2477 fix_aligned(content),
2478 "- this is a long line\n that continues on a second line\n"
2479 );
2480 }
2481
2482 #[test]
2483 fn aligned_full_issue_example_made_consistent() {
2484 let content = "- this is a long line\n\
2487 that continues on a second line\n\
2488 - this is another long line\n\
2489 \x20\x20that continues on the next line\n\
2490 - yet again a long line\n\
2491 and still inconsistently spaced\n\
2492 \x20\x20and even worse\n";
2493 let expected = "- this is a long line\n\
2494 \x20\x20that continues on a second line\n\
2495 - this is another long line\n\
2496 \x20\x20that continues on the next line\n\
2497 - yet again a long line\n\
2498 \x20\x20and still inconsistently spaced\n\
2499 \x20\x20and even worse\n";
2500 assert_eq!(fix_aligned(content), expected);
2501 assert_eq!(fix_aligned(expected), expected);
2503 }
2504
2505 #[test]
2506 fn aligned_already_aligned_not_flagged() {
2507 let content = "- item\n continuation at content column\n";
2508 assert!(check_aligned(content).is_empty());
2509 }
2510
2511 #[test]
2512 fn aligned_tight_partial_indent_flagged() {
2513 let content = "- item\n continuation\n";
2515 let warnings = check_aligned(content);
2516 assert_eq!(warnings.len(), 1);
2517 assert_eq!(fix_aligned(content), "- item\n continuation\n");
2518 }
2519
2520 #[test]
2521 fn aligned_post_blank_zero_indent_still_new_paragraph() {
2522 let content = "- item\n\nnew paragraph\n";
2525 assert!(check_aligned(content).is_empty());
2526 assert_eq!(fix_aligned(content), content);
2527 }
2528
2529 #[test]
2532 fn aligned_top_level_blockquote_after_list_untouched() {
2533 let content = "- item\n> quote\n";
2537 assert!(check_aligned(content).is_empty());
2538 assert_eq!(fix_aligned(content), content);
2539 }
2540
2541 #[test]
2542 fn aligned_top_level_fence_after_list_untouched() {
2543 let content = "- item\n```\ncode\n```\n";
2544 assert!(check_aligned(content).is_empty());
2545 assert_eq!(fix_aligned(content), content);
2546 }
2547
2548 #[test]
2549 fn aligned_top_level_table_after_list_untouched() {
2550 let content = "- item\n| a | b |\n|---|---|\n| 1 | 2 |\n";
2551 assert!(check_aligned(content).is_empty());
2552 assert_eq!(fix_aligned(content), content);
2553 }
2554
2555 #[test]
2558 fn aligned_nested_tight_lazy_continuation_aligns_to_inner() {
2559 let content = "- Outer\n - Inner\ncontinuation\n";
2564 let warnings = check_aligned(content);
2565 assert_eq!(warnings.len(), 1);
2566 assert_eq!(fix_aligned(content), "- Outer\n - Inner\n continuation\n");
2567 }
2568
2569 #[test]
2570 fn aligned_nested_continuation_already_aligned_not_flagged() {
2571 let content = "- L1\n - L2\n cont of L2 at 4\n";
2572 assert!(check_aligned(content).is_empty());
2573 }
2574
2575 #[test]
2576 fn aligned_nested_idempotent() {
2577 let content = "- Outer\n - Inner\ncontinuation\n";
2578 let once = fix_aligned(content);
2579 assert_eq!(fix_aligned(&once), once);
2580 }
2581
2582 #[test]
2583 fn aligned_three_level_nesting_aligns_to_innermost() {
2584 let content = "- L1\n - L2\n - L3\ncont\n";
2587 assert_eq!(fix_aligned(content), "- L1\n - L2\n - L3\n cont\n");
2588 }
2589
2590 #[test]
2591 fn aligned_continuation_after_sibling_owned_by_last_item() {
2592 let content = "- a\n- b\nlazy\n";
2595 assert_eq!(fix_aligned(content), "- a\n- b\n lazy\n");
2596 }
2597
2598 #[test]
2599 fn aligned_multi_digit_ordered_marker_aligns_to_content_column() {
2600 let content = "10. Item\nwrap\n";
2601 assert_eq!(fix_aligned(content), "10. Item\n wrap\n");
2602 }
2603
2604 #[test]
2605 fn aligned_setext_heading_after_list_left_alone() {
2606 let content = "- item\nText\n===\n";
2609 assert!(check_aligned(content).is_empty());
2610 assert_eq!(fix_aligned(content), content);
2611 }
2612
2613 #[test]
2614 fn aligned_latent_marker_in_continuation_is_idempotent() {
2615 let content = "# \n- \n``\n2. \n![]()";
2621 let once = fix_aligned(content);
2622 assert_eq!(fix_aligned(&once), once, "fix must be idempotent in one pass");
2623 assert_eq!(once, content, "item with a latent marker is left untouched");
2624 }
2625
2626 #[test]
2627 fn aligned_latent_table_in_continuation_is_idempotent() {
2628 let content = "- \n![`]()\n| | ` |\n| --- | --- |";
2633 let once = fix_aligned(content);
2634 assert_eq!(fix_aligned(&once), once, "fix must be idempotent in one pass");
2635 assert_eq!(once, content, "item with a latent table is left untouched");
2636 }
2637
2638 #[test]
2639 fn aligned_blockquote_nested_list_not_touched() {
2640 let content = "> - item\n> wrap\n";
2644 assert!(check_aligned(content).is_empty());
2645 assert_eq!(fix_aligned(content), content);
2646 }
2647
2648 #[test]
2651 fn aligned_task_post_checkbox_column_accepted() {
2652 let content = "- [ ] Task\n wrap\n";
2655 assert!(check_aligned(content).is_empty());
2656 assert_eq!(fix_aligned(content), content);
2657 }
2658
2659 #[test]
2660 fn aligned_task_under_indent_snaps_to_content_column() {
2661 let content = "- [ ] Task\nwrap\n";
2662 let warnings = check_aligned(content);
2663 assert_eq!(warnings.len(), 1);
2664 assert_eq!(fix_aligned(content), "- [ ] Task\n wrap\n");
2665 }
2666
2667 #[test]
2670 fn aligned_mkdocs_tight_under_indent_snaps_to_four() {
2671 let content = "- item\nwrap\n";
2673 let warnings = check_aligned_mkdocs(content);
2674 assert_eq!(warnings.len(), 1);
2675 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
2676 assert_eq!(aligned_rule().fix(&ctx).unwrap(), "- item\n wrap\n");
2677 }
2678
2679 #[test]
2682 fn any_default_does_not_flag_tight_lazy_continuation() {
2683 let content = "- item\nwrapped at zero indent\n";
2685 assert!(check(content).is_empty());
2686 assert_eq!(fix(content), content);
2687 }
2688
2689 #[test]
2690 fn from_config_aligned_enables_tight_flagging() {
2691 let mut config = crate::config::Config::default();
2693 let mut rule_config = crate::config::RuleConfig::default();
2694 rule_config
2695 .values
2696 .insert("style".to_string(), toml::Value::String("aligned".to_string()));
2697 config.rules.insert("MD077".to_string(), rule_config);
2698
2699 let rule = MD077ListContinuationIndent::from_config(&config);
2700 let ctx = LintContext::new("- item\nwrap\n", MarkdownFlavor::Standard, None);
2701 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
2702 }
2703
2704 #[test]
2705 fn from_config_default_is_any() {
2706 let config = crate::config::Config::default();
2708 let rule = MD077ListContinuationIndent::from_config(&config);
2709 let ctx = LintContext::new("- item\nwrap\n", MarkdownFlavor::Standard, None);
2710 assert!(rule.check(&ctx).unwrap().is_empty());
2711 }
2712
2713 #[test]
2714 fn aligned_tight_underindented_fence_inside_item_left_alone() {
2715 let content = "- item\n ```\n code\n ```\n";
2719 assert!(check_aligned(content).is_empty());
2720 assert_eq!(fix_aligned(content), content);
2721 }
2722
2723 #[test]
2724 fn aligned_task_under_indent_fix_is_idempotent() {
2725 let content = "- [ ] Task\nwrap\n";
2726 let once = fix_aligned(content);
2727 assert_eq!(fix_aligned(&once), once);
2728 }
2729
2730 #[test]
2731 fn aligned_partial_indent_fix_is_idempotent() {
2732 let content = "- item\n continuation\n";
2733 let once = fix_aligned(content);
2734 assert_eq!(fix_aligned(&once), once);
2735 }
2736}