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 mut range_ends = vec![total_lines; items.len()];
609 let mut stack: Vec<usize> = Vec::new();
610 for i in (0..items.len()).rev() {
611 let marker_col = items[i].1;
612 while let Some(&top) = stack.last() {
613 if items[top].1 > marker_col {
614 stack.pop();
615 } else {
616 break;
617 }
618 }
619 range_ends[i] = stack.last().map_or(total_lines, |&j| items[j].0 - 1);
620 stack.push(i);
621 }
622
623 let scoped: Vec<(usize, usize, usize, Option<usize>, usize, usize)> = items
626 .iter()
627 .enumerate()
628 .map(|(item_idx, &(item_line, marker_col, content_col, task_col))| {
629 let required = if strict_indent { content_col.max(4) } else { content_col };
630 (
631 item_line,
632 marker_col,
633 content_col,
634 task_col,
635 required,
636 range_ends[item_idx],
637 )
638 })
639 .collect();
640
641 let prose_candidate_lines: Vec<usize> = (1..=total_lines)
655 .filter(|&line_num| {
656 let Some(info) = ctx.line_info(line_num) else {
657 return false;
658 };
659 let trimmed = info.content(ctx.content).trim_start();
660 !Self::should_skip_line(info, trimmed)
661 && !info.is_blank
662 && info.list_item.is_none()
663 && info.heading.is_none()
664 && !info.is_horizontal_rule
665 && !Self::is_block_level_construct(trimmed)
666 })
667 .collect();
668 let range_has_prose_candidate = |after_line: usize, range_end: usize| -> bool {
671 let start = prose_candidate_lines.partition_point(|&l| l <= after_line);
672 prose_candidate_lines.get(start).is_some_and(|&l| l <= range_end)
673 };
674
675 let aligned = self.config.style == ContinuationStyle::Aligned;
701 for &(item_line, marker_col, _content_col, _task_col, required, range_end) in &scoped {
702 if !range_has_prose_candidate(item_line, range_end) {
705 continue;
706 }
707 let has_latent_structure = aligned && Self::item_range_has_latent_structure(ctx, item_line, range_end);
722 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
723 let actual = line.actual;
724 let under_indented = actual < required;
725 let loose_escape = line.saw_blank && under_indented;
726 let confirmed_structure = line.info.in_code_block || line.info.blockquote.is_some();
733 let aligned_tight = aligned
734 && !has_latent_structure
735 && !line.saw_blank
736 && !line.saw_nested
737 && under_indented
738 && !confirmed_structure;
739 if (loose_escape || aligned_tight) && flagged_lines.insert(line.line_num) {
740 let message = if line.saw_blank {
741 if strict_indent {
742 format!(
743 "Content inside list item needs {required} spaces of indentation \
744 for MkDocs compatibility (found {actual})",
745 )
746 } else {
747 format!(
748 "Content after blank line in list item needs {required} spaces of \
749 indentation to remain part of the list (found {actual})",
750 )
751 }
752 } else {
753 format!("Continuation line under-indented (expected {required}, found {actual})")
754 };
755 let outcome = Self::build_under_indent_warning(ctx, line, required, message);
756 if let Some(closer_line) = outcome.also_flag_line {
757 flagged_lines.insert(closer_line);
758 }
759 warnings.push(outcome.warning);
760 }
761 ControlFlow::Continue(())
762 });
763 }
764
765 for &(item_line, marker_col, content_col, task_col, required, range_end) in &scoped {
774 if !range_has_prose_candidate(item_line, range_end) {
776 continue;
777 }
778 let (uses_content_col, uses_task_col) = match task_col {
782 Some(t) => Self::sibling_column_usage(ctx, item_line, range_end, marker_col, content_col, t),
783 None => (false, false),
784 };
785
786 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
787 let actual = line.actual;
788 if actual > required
789 && !line.info.in_code_block
790 && Some(actual) != task_col
791 && !Self::starts_with_list_marker(line.trimmed)
792 && flagged_lines.insert(line.line_num)
793 {
794 let fix_target =
795 Self::compute_fix_target(actual, required, task_col, uses_content_col, uses_task_col);
796 let message = match task_col {
797 Some(t) => format!(
798 "Continuation line over-indented \
799 (expected {required} or {t}, found {actual})"
800 ),
801 None => {
802 format!("Continuation line over-indented (expected {required}, found {actual})")
803 }
804 };
805 warnings.push(Self::build_over_indent_warning(ctx, line, fix_target, message));
806 }
807 ControlFlow::Continue(())
808 });
809 }
810
811 warnings.sort_by_key(|w| (w.line, w.column));
814
815 Ok(warnings)
816 }
817
818 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
819 let warnings = self.check(ctx)?;
820 let warnings =
821 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
822 if warnings.is_empty() {
823 return Ok(ctx.content.to_string());
824 }
825
826 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
828 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
829
830 let mut content = ctx.content.to_string();
831 for fix in fixes {
832 if fix.range.start <= content.len() && fix.range.end <= content.len() {
833 content.replace_range(fix.range, &fix.replacement);
834 }
835 }
836
837 Ok(content)
838 }
839
840 fn category(&self) -> RuleCategory {
841 RuleCategory::List
842 }
843
844 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
845 ctx.content.is_empty() || ctx.list_blocks.is_empty()
846 }
847
848 fn as_any(&self) -> &dyn std::any::Any {
849 self
850 }
851
852 crate::impl_rule_config_methods!(MD077Config);
853}
854
855#[cfg(test)]
856mod tests {
857 use super::*;
858 use crate::config::MarkdownFlavor;
859
860 fn check(content: &str) -> Vec<LintWarning> {
861 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
862 let rule = MD077ListContinuationIndent::default();
863 rule.check(&ctx).unwrap()
864 }
865
866 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
867 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
868 let rule = MD077ListContinuationIndent::default();
869 rule.check(&ctx).unwrap()
870 }
871
872 fn fix(content: &str) -> String {
873 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
874 let rule = MD077ListContinuationIndent::default();
875 rule.fix(&ctx).unwrap()
876 }
877
878 fn fix_mkdocs(content: &str) -> String {
879 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
880 let rule = MD077ListContinuationIndent::default();
881 rule.fix(&ctx).unwrap()
882 }
883
884 fn aligned_rule() -> MD077ListContinuationIndent {
885 MD077ListContinuationIndent::new(ContinuationStyle::Aligned)
886 }
887
888 fn check_aligned(content: &str) -> Vec<LintWarning> {
889 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
890 aligned_rule().check(&ctx).unwrap()
891 }
892
893 fn check_aligned_mkdocs(content: &str) -> Vec<LintWarning> {
894 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
895 aligned_rule().check(&ctx).unwrap()
896 }
897
898 fn fix_aligned(content: &str) -> String {
899 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
900 aligned_rule().fix(&ctx).unwrap()
901 }
902
903 fn fix_aligned_quarto(content: &str) -> String {
904 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
905 aligned_rule().fix(&ctx).unwrap()
906 }
907
908 #[test]
909 fn aligned_idempotent_with_latent_marker_behind_unstable_heading() {
910 let input = "1. \n``\n``\n- \n``";
919 let once = fix_aligned_quarto(input);
920 let twice = fix_aligned_quarto(&once);
921 assert_eq!(once, twice, "MD077 aligned fix must be idempotent (Quarto)");
922 }
923
924 #[test]
927 fn tight_lazy_continuation_zero_indent_not_flagged() {
928 let content = "- Item\ncontinuation\n";
930 assert!(check(content).is_empty());
931 }
932
933 #[test]
934 fn tight_continuation_correct_indent_not_flagged() {
935 let content = "1. Item\n continuation\n";
937 assert!(check(content).is_empty());
938 }
939
940 #[test]
941 fn tight_continuation_over_indented_ordered() {
942 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
944 let warnings = check(content);
945 assert_eq!(warnings.len(), 1);
946 assert_eq!(warnings[0].line, 2);
947 assert!(warnings[0].message.contains("over-indented"));
948 }
949
950 #[test]
951 fn tight_continuation_over_indented_unordered() {
952 let content = "- Item\n over-indented\n";
954 let warnings = check(content);
955 assert_eq!(warnings.len(), 1);
956 assert_eq!(warnings[0].line, 2);
957 }
958
959 #[test]
960 fn tight_continuation_multiple_over_indented_lines() {
961 let content = "1. Item\n line one\n line two\n line three\n";
962 let warnings = check(content);
963 assert_eq!(warnings.len(), 3);
964 }
965
966 #[test]
967 fn tight_continuation_mixed_correct_and_over() {
968 let content = "1. Item\n correct\n over-indented\n correct again\n";
969 let warnings = check(content);
970 assert_eq!(warnings.len(), 1);
971 assert_eq!(warnings[0].line, 3);
972 }
973
974 #[test]
975 fn tight_continuation_nested_over_indented() {
976 let content = "- L1\n - L2\n over-indented continuation of L2\n";
978 let warnings = check(content);
979 assert_eq!(warnings.len(), 1);
980 assert_eq!(warnings[0].line, 3);
981 assert!(warnings[0].message.contains("expected 4"));
983 assert!(warnings[0].message.contains("found 5"));
984 }
985
986 #[test]
987 fn tight_continuation_nested_correct_indent_not_flagged() {
988 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
991 assert!(check(content).is_empty());
992 }
993
994 #[test]
995 fn fix_tight_continuation_nested_over_indented() {
996 let content = "- L1\n - L2\n over-indented continuation of L2\n";
998 let fixed = fix(content);
999 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
1000 }
1001
1002 #[test]
1003 fn tight_continuation_under_indented_not_flagged() {
1004 let content = "1. Item\n under-indented\n";
1007 assert!(check(content).is_empty());
1008 }
1009
1010 #[test]
1011 fn tight_continuation_tab_over_indented() {
1012 let content = "- Item\n\tover-indented\n";
1014 let warnings = check(content);
1015 assert_eq!(warnings.len(), 1);
1016 }
1017
1018 #[test]
1019 fn fix_tight_continuation_over_indented_ordered() {
1020 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
1021 let fixed = fix(content);
1022 assert_eq!(
1023 fixed,
1024 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
1025 );
1026 }
1027
1028 #[test]
1029 fn fix_tight_continuation_over_indented_unordered() {
1030 let content = "- Item\n over-indented\n";
1031 let fixed = fix(content);
1032 assert_eq!(fixed, "- Item\n over-indented\n");
1033 }
1034
1035 #[test]
1036 fn fix_tight_continuation_multiple_lines() {
1037 let content = "1. Item\n line one\n line two\n";
1038 let fixed = fix(content);
1039 assert_eq!(fixed, "1. Item\n line one\n line two\n");
1040 }
1041
1042 #[test]
1043 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
1044 let content = "1. Item\n continuation\n";
1047 assert!(check_mkdocs(content).is_empty());
1048 }
1049
1050 #[test]
1051 fn tight_continuation_mkdocs_5space_ordered_flagged() {
1052 let content = "1. Item\n over-indented\n";
1054 let warnings = check_mkdocs(content);
1055 assert_eq!(warnings.len(), 1);
1056 assert!(warnings[0].message.contains("expected 4"));
1057 assert!(warnings[0].message.contains("found 5"));
1058 }
1059
1060 #[test]
1061 fn fix_tight_continuation_mkdocs_over_indented() {
1062 let content = "1. Item\n over-indented\n";
1063 let fixed = fix_mkdocs(content);
1064 assert_eq!(fixed, "1. Item\n over-indented\n");
1065 }
1066
1067 #[test]
1068 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
1069 let content = "* Level 0\n * Level 1\n * Level 2\n";
1072 assert!(check(content).is_empty());
1073 }
1074
1075 #[test]
1076 fn tight_continuation_ordered_marker_not_flagged() {
1077 let content = "- Parent\n 1. Child item\n";
1079 assert!(check(content).is_empty());
1080 }
1081
1082 #[test]
1085 fn unordered_correct_indent_no_warning() {
1086 let content = "- Item\n\n continuation\n";
1087 assert!(check(content).is_empty());
1088 }
1089
1090 #[test]
1091 fn unordered_partial_indent_warns() {
1092 let content = "- Item\n\n continuation\n";
1095 let warnings = check(content);
1096 assert_eq!(warnings.len(), 1);
1097 assert_eq!(warnings[0].line, 3);
1098 assert!(warnings[0].message.contains("2 spaces"));
1099 assert!(warnings[0].message.contains("found 1"));
1100 }
1101
1102 #[test]
1103 fn unordered_zero_indent_is_new_paragraph() {
1104 let content = "- Item\n\ncontinuation\n";
1107 assert!(check(content).is_empty());
1108 }
1109
1110 #[test]
1113 fn ordered_3space_correct_commonmark() {
1114 let content = "1. Item\n\n continuation\n";
1116 assert!(check(content).is_empty());
1117 }
1118
1119 #[test]
1120 fn ordered_2space_under_indent_commonmark() {
1121 let content = "1. Item\n\n continuation\n";
1122 let warnings = check(content);
1123 assert_eq!(warnings.len(), 1);
1124 assert!(warnings[0].message.contains("3 spaces"));
1125 assert!(warnings[0].message.contains("found 2"));
1126 }
1127
1128 #[test]
1131 fn multi_digit_marker_correct() {
1132 let content = "10. Item\n\n continuation\n";
1134 assert!(check(content).is_empty());
1135 }
1136
1137 #[test]
1138 fn multi_digit_marker_under_indent() {
1139 let content = "10. Item\n\n continuation\n";
1140 let warnings = check(content);
1141 assert_eq!(warnings.len(), 1);
1142 assert!(warnings[0].message.contains("4 spaces"));
1143 }
1144
1145 #[test]
1148 fn mkdocs_3space_ordered_warns() {
1149 let content = "1. Item\n\n continuation\n";
1151 let warnings = check_mkdocs(content);
1152 assert_eq!(warnings.len(), 1);
1153 assert!(warnings[0].message.contains("4 spaces"));
1154 assert!(warnings[0].message.contains("MkDocs"));
1155 }
1156
1157 #[test]
1158 fn mkdocs_4space_ordered_no_warning() {
1159 let content = "1. Item\n\n continuation\n";
1160 assert!(check_mkdocs(content).is_empty());
1161 }
1162
1163 #[test]
1164 fn mkdocs_unordered_2space_ok() {
1165 let content = "- Item\n\n continuation\n";
1167 assert!(check_mkdocs(content).is_empty());
1168 }
1169
1170 #[test]
1171 fn mkdocs_unordered_2space_warns() {
1172 let content = "- Item\n\n continuation\n";
1174 let warnings = check_mkdocs(content);
1175 assert_eq!(warnings.len(), 1);
1176 assert!(warnings[0].message.contains("4 spaces"));
1177 }
1178
1179 #[test]
1182 fn fix_unordered_indent() {
1183 let content = "- Item\n\n continuation\n";
1185 let fixed = fix(content);
1186 assert_eq!(fixed, "- Item\n\n continuation\n");
1187 }
1188
1189 #[test]
1190 fn fix_ordered_indent() {
1191 let content = "1. Item\n\n continuation\n";
1192 let fixed = fix(content);
1193 assert_eq!(fixed, "1. Item\n\n continuation\n");
1194 }
1195
1196 #[test]
1197 fn fix_mkdocs_indent() {
1198 let content = "1. Item\n\n continuation\n";
1199 let fixed = fix_mkdocs(content);
1200 assert_eq!(fixed, "1. Item\n\n continuation\n");
1201 }
1202
1203 #[test]
1206 fn nested_list_items_not_flagged() {
1207 let content = "- Parent\n\n - Child\n";
1208 assert!(check(content).is_empty());
1209 }
1210
1211 #[test]
1212 fn nested_list_zero_indent_is_new_paragraph() {
1213 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
1215 assert!(check(content).is_empty());
1216 }
1217
1218 #[test]
1219 fn nested_list_partial_indent_flagged() {
1220 let content = "- Parent\n - Child\n\n continuation of parent\n";
1222 let warnings = check(content);
1223 assert_eq!(warnings.len(), 1);
1224 assert!(warnings[0].message.contains("2 spaces"));
1225 }
1226
1227 #[test]
1230 fn code_block_correctly_indented_no_warning() {
1231 let content = "- Item\n\n ```\n code\n ```\n";
1233 assert!(check(content).is_empty());
1234 }
1235
1236 #[test]
1237 fn code_fence_under_indented_warns() {
1238 let content = "- Item\n\n ```\n code\n ```\n";
1242 let warnings = check(content);
1243 assert_eq!(warnings.len(), 1);
1244 assert_eq!(warnings[0].line, 3);
1245 }
1246
1247 #[test]
1248 fn code_fence_under_indented_ordered_mkdocs() {
1249 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
1252 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
1254 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1256 assert!(warnings[0].message.contains("4 spaces"));
1257 assert!(warnings[0].message.contains("MkDocs"));
1258 }
1259
1260 #[test]
1261 fn code_fence_tilde_under_indented() {
1262 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1263 let warnings = check(content);
1264 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1266 }
1267
1268 #[test]
1271 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
1272 let content = "- Item\n\n\ncontinuation\n";
1274 assert!(check(content).is_empty());
1275 }
1276
1277 #[test]
1278 fn multiple_blank_lines_partial_indent_flags() {
1279 let content = "- Item\n\n\n continuation\n";
1280 let warnings = check(content);
1281 assert_eq!(warnings.len(), 1);
1282 }
1283
1284 #[test]
1287 fn empty_item_no_warning() {
1288 let content = "- \n- Second\n";
1289 assert!(check(content).is_empty());
1290 }
1291
1292 #[test]
1295 fn multiple_items_mixed_indent() {
1296 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
1297 let warnings = check(content);
1298 assert_eq!(warnings.len(), 1);
1299 assert_eq!(warnings[0].line, 7);
1300 }
1301
1302 #[test]
1305 fn task_list_correct_indent() {
1306 let content = "- [ ] Task\n\n continuation\n";
1308 assert!(check(content).is_empty());
1309 }
1310
1311 #[test]
1314 fn frontmatter_not_flagged() {
1315 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
1316 assert!(check(content).is_empty());
1317 }
1318
1319 #[test]
1322 fn fix_multiple_items() {
1323 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
1324 let fixed = fix(content);
1325 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
1326 }
1327
1328 #[test]
1329 fn fix_multiline_loose_continuation_all_lines() {
1330 let content = "1. Item\n\n line one\n line two\n line three\n";
1331 let fixed = fix(content);
1332 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
1333 }
1334
1335 #[test]
1338 fn sibling_item_boundary_respected() {
1339 let content = "- First\n- Second\n\n continuation\n";
1341 assert!(check(content).is_empty());
1342 }
1343
1344 #[test]
1347 fn blockquote_list_correct_indent_no_warning() {
1348 let content = "> - Item\n>\n> continuation\n";
1351 assert!(check(content).is_empty());
1352 }
1353
1354 #[test]
1355 fn blockquote_list_under_indent_no_false_positive() {
1356 let content = "> - Item\n>\n> continuation\n";
1361 assert!(check(content).is_empty());
1362 }
1363
1364 #[test]
1367 fn deeply_nested_correct_indent() {
1368 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1369 assert!(check(content).is_empty());
1370 }
1371
1372 #[test]
1373 fn deeply_nested_under_indent() {
1374 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1377 let warnings = check(content);
1378 assert_eq!(warnings.len(), 1);
1379 assert!(warnings[0].message.contains("6 spaces"));
1380 assert!(warnings[0].message.contains("found 5"));
1381 }
1382
1383 #[test]
1386 fn loose_tab_continuation_over_indented() {
1387 let content = "- Item\n\n\tcontinuation\n";
1392 let warnings = check(content);
1393 assert_eq!(warnings.len(), 1);
1394 assert_eq!(warnings[0].line, 3);
1395 assert_eq!(fix(content), "- Item\n\n continuation\n");
1396 }
1397
1398 #[test]
1401 fn multiple_continuations_correct() {
1402 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
1403 assert!(check(content).is_empty());
1404 }
1405
1406 #[test]
1407 fn multiple_continuations_second_under_indent() {
1408 let content = "- Item\n\n para 1\n\n continuation 2\n";
1410 let warnings = check(content);
1411 assert_eq!(warnings.len(), 1);
1412 assert_eq!(warnings[0].line, 5);
1413 }
1414
1415 #[test]
1418 fn ordered_paren_marker_correct() {
1419 let content = "1) Item\n\n continuation\n";
1421 assert!(check(content).is_empty());
1422 }
1423
1424 #[test]
1425 fn ordered_paren_marker_under_indent() {
1426 let content = "1) Item\n\n continuation\n";
1427 let warnings = check(content);
1428 assert_eq!(warnings.len(), 1);
1429 assert!(warnings[0].message.contains("3 spaces"));
1430 }
1431
1432 #[test]
1435 fn star_marker_correct() {
1436 let content = "* Item\n\n continuation\n";
1437 assert!(check(content).is_empty());
1438 }
1439
1440 #[test]
1441 fn star_marker_under_indent() {
1442 let content = "* Item\n\n continuation\n";
1443 let warnings = check(content);
1444 assert_eq!(warnings.len(), 1);
1445 }
1446
1447 #[test]
1448 fn plus_marker_correct() {
1449 let content = "+ Item\n\n continuation\n";
1450 assert!(check(content).is_empty());
1451 }
1452
1453 #[test]
1456 fn heading_after_list_no_warning() {
1457 let content = "- Item\n\n# Heading\n";
1458 assert!(check(content).is_empty());
1459 }
1460
1461 #[test]
1464 fn hr_after_list_no_warning() {
1465 let content = "- Item\n\n---\n";
1466 assert!(check(content).is_empty());
1467 }
1468
1469 #[test]
1472 fn reference_link_def_not_flagged() {
1473 let content = "- Item\n\n [link]: https://example.com\n";
1474 assert!(check(content).is_empty());
1475 }
1476
1477 #[test]
1480 fn footnote_def_not_flagged() {
1481 let content = "- Item\n\n [^1]: footnote text\n";
1482 assert!(check(content).is_empty());
1483 }
1484
1485 #[test]
1486 fn footnote_multiline_body_after_list_not_flagged() {
1487 let content = "# A list followed by a footnote\n\n\
1491 Here is a paragraph.[^fn]\n\n\
1492 - This is a list.\n\n\
1493 [^fn]:\n\
1494 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1495 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1496 assert!(check(content).is_empty());
1497 }
1498
1499 #[test]
1500 fn fix_footnote_multiline_body_after_list_is_noop() {
1501 let content = "# A list followed by a footnote\n\n\
1505 Here is a paragraph.[^fn]\n\n\
1506 - This is a list.\n\n\
1507 [^fn]:\n\
1508 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1509 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1510 assert_eq!(fix(content), content);
1511 }
1512
1513 #[test]
1514 fn footnote_body_indented_past_list_content_col_not_flagged() {
1515 let content = "- Item\n\n[^fn]:\n Body line one.\n Body line two.\n";
1519 assert!(check(content).is_empty());
1520 }
1521
1522 #[test]
1523 fn list_inside_footnote_body_continuation_not_flagged() {
1524 let content = "Text.[^fn]\n\n[^fn]:\n\
1528 \x20\x20\x20\x20- nested item\n\
1529 \x20\x20\x20\x20\x20\x20\x20over-indented continuation\n";
1530 assert!(check(content).is_empty());
1531 }
1532
1533 #[test]
1534 fn footnote_multiline_body_after_list_not_flagged_mkdocs() {
1535 let content = "Here is a paragraph.[^fn]\n\n\
1539 - This is a list.\n\n\
1540 [^fn]:\n\
1541 \x20\x20\x20\x20\x20\x20Footnote body that spans\n\
1542 \x20\x20\x20\x20\x20\x20multiple indented lines.\n";
1543 assert!(check_mkdocs(content).is_empty());
1544 }
1545
1546 #[test]
1549 fn fix_deeply_nested() {
1550 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
1551 let fixed = fix(content);
1552 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
1553 }
1554
1555 #[test]
1556 fn fix_mkdocs_unordered() {
1557 let content = "- Item\n\n continuation\n";
1559 let fixed = fix_mkdocs(content);
1560 assert_eq!(fixed, "- Item\n\n continuation\n");
1561 }
1562
1563 #[test]
1564 fn fix_code_fence_indent() {
1565 let content = "- Item\n\n ```\n code\n ```\n";
1568 let fixed = fix(content);
1569 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
1570 }
1571
1572 #[test]
1573 fn fix_mkdocs_code_fence_indent() {
1574 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
1576 let fixed = fix_mkdocs(content);
1577 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
1578 }
1579
1580 #[test]
1583 fn empty_document_no_warning() {
1584 assert!(check("").is_empty());
1585 }
1586
1587 #[test]
1588 fn whitespace_only_no_warning() {
1589 assert!(check(" \n\n \n").is_empty());
1590 }
1591
1592 #[test]
1595 fn no_list_no_warning() {
1596 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
1597 assert!(check(content).is_empty());
1598 }
1599
1600 #[test]
1603 fn multiline_continuation_all_lines_flagged() {
1604 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";
1605 let warnings = check(content);
1606 assert_eq!(warnings.len(), 3);
1607 assert_eq!(warnings[0].line, 3);
1608 assert_eq!(warnings[1].line, 4);
1609 assert_eq!(warnings[2].line, 5);
1610 }
1611
1612 #[test]
1613 fn multiline_continuation_with_frontmatter_fix() {
1614 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";
1615 let fixed = fix(content);
1616 assert_eq!(
1617 fixed,
1618 "---\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"
1619 );
1620 }
1621
1622 #[test]
1623 fn multiline_continuation_correct_indent_no_warning() {
1624 let content = "1. Item\n\n line one\n line two\n line three\n";
1625 assert!(check(content).is_empty());
1626 }
1627
1628 #[test]
1629 fn multiline_continuation_mixed_indent() {
1630 let content = "1. Item\n\n correct\n wrong\n correct\n";
1631 let warnings = check(content);
1632 assert_eq!(warnings.len(), 1);
1633 assert_eq!(warnings[0].line, 4);
1634 }
1635
1636 #[test]
1637 fn multiline_continuation_unordered() {
1638 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
1639 let warnings = check(content);
1640 assert_eq!(warnings.len(), 3);
1641 let fixed = fix(content);
1642 assert_eq!(
1643 fixed,
1644 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
1645 );
1646 }
1647
1648 #[test]
1649 fn multiline_continuation_two_items_fix() {
1650 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
1651 let fixed = fix(content);
1652 assert_eq!(
1653 fixed,
1654 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1655 );
1656 }
1657
1658 #[test]
1659 fn fence_fix_does_not_break_pairing_for_md031() {
1660 let content = "#### title\n\nabc\n\n\
1667 1. ab\n\n\
1668 \x20\x20`aabbccdd`\n\n\
1669 2. cd\n\n\
1670 \x20\x20`bbcc dd ee`\n\n\
1671 \x20\x20```\n\
1672 \x20\x20abcd\n\
1673 \x20\x20ef gh\n\
1674 \x20\x20```\n\n\
1675 \x20\x20uu\n\n\
1676 \x20\x20```\n\
1677 \x20\x20cdef\n\
1678 \x20\x20gh ij\n\
1679 \x20\x20```\n";
1680 let expected = "#### title\n\nabc\n\n\
1681 1. ab\n\n\
1682 \x20\x20\x20`aabbccdd`\n\n\
1683 2. cd\n\n\
1684 \x20\x20\x20`bbcc dd ee`\n\n\
1685 \x20\x20\x20```\n\
1686 \x20\x20\x20abcd\n\
1687 \x20\x20\x20ef gh\n\
1688 \x20\x20\x20```\n\n\
1689 \x20\x20\x20uu\n\n\
1690 \x20\x20\x20```\n\
1691 \x20\x20\x20cdef\n\
1692 \x20\x20\x20gh ij\n\
1693 \x20\x20\x20```\n";
1694 assert_eq!(fix(content), expected);
1695 }
1696
1697 #[test]
1698 fn multiline_continuation_separated_by_blank() {
1699 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1700 let warnings = check(content);
1701 assert_eq!(warnings.len(), 4);
1702 let fixed = fix(content);
1703 assert_eq!(
1704 fixed,
1705 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1706 );
1707 }
1708
1709 #[test]
1710 fn tab_indented_fence_is_normalized_to_spaces() {
1711 let content = "100. ab\n\n\t```\n\tabcd\n\t```\n";
1719 let expected = "100. ab\n\n ```\n abcd\n ```\n";
1720 assert_eq!(fix(content), expected);
1721 }
1722
1723 #[test]
1732 fn loose_continuation_over_indented_flagged() {
1733 let content = "* Item\n\n over-indented\n";
1736 let warnings = check(content);
1737 assert_eq!(warnings.len(), 1);
1738 assert_eq!(warnings[0].line, 3);
1739 assert!(warnings[0].message.contains("over-indented"));
1740 assert!(warnings[0].message.contains("expected 2"));
1741 assert!(warnings[0].message.contains("found 3"));
1742 }
1743
1744 #[test]
1745 fn loose_continuation_over_indented_multiline_mixed() {
1746 let content = "* Item\n\n over one\n correct\n over two\n";
1748 let warnings = check(content);
1749 assert_eq!(warnings.len(), 2);
1750 assert_eq!(warnings[0].line, 3);
1751 assert_eq!(warnings[1].line, 5);
1752 }
1753
1754 #[test]
1755 fn fix_loose_continuation_over_indented() {
1756 let content = "* Item\n\n over one\n correct\n over two\n";
1757 let fixed = fix(content);
1758 assert_eq!(fixed, "* Item\n\n over one\n correct\n over two\n");
1759 }
1760
1761 #[test]
1762 fn fix_tight_and_loose_items_normalized_identically() {
1763 let content = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1766 * 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\
1767 * 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";
1768 let expected = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1769 * 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\
1770 * 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";
1771 assert_eq!(fix(content), expected);
1772 }
1773
1774 #[test]
1775 fn multi_paragraph_item_loose_paragraph_over_indented() {
1776 let content = "* Item.\n tight over\n\n loose over\n";
1779 let warnings = check(content);
1780 assert_eq!(warnings.len(), 2);
1781 assert_eq!(warnings[0].line, 2);
1782 assert_eq!(warnings[1].line, 4);
1783 }
1784
1785 #[test]
1786 fn loose_indented_code_block_not_flagged() {
1787 let content = "- Item\n\n code line\n";
1791 assert!(check(content).is_empty());
1792 }
1793
1794 #[test]
1795 fn mkdocs_loose_over_indented_flagged() {
1796 let content = "1. Item\n\n over\n";
1799 let warnings = check_mkdocs(content);
1800 assert_eq!(warnings.len(), 1);
1801 assert_eq!(warnings[0].line, 3);
1802 assert!(warnings[0].message.contains("over-indented"));
1803 assert!(warnings[0].message.contains("expected 4"));
1804 assert!(warnings[0].message.contains("found 5"));
1805 }
1806
1807 #[test]
1808 fn task_list_loose_over_indented_flagged() {
1809 let content = "- [ ] Task\n\n over\n";
1812 let warnings = check(content);
1813 assert_eq!(warnings.len(), 1);
1814 assert_eq!(warnings[0].line, 3);
1815 }
1816
1817 #[test]
1818 fn loose_over_indent_boundary_below_code_block_threshold_flagged() {
1819 let content = "- Item\n\n over\n";
1824 let warnings = check(content);
1825 assert_eq!(warnings.len(), 1);
1826 assert_eq!(warnings[0].line, 3);
1827 assert!(warnings[0].message.contains("expected 2"));
1828 assert!(warnings[0].message.contains("found 5"));
1829 }
1830
1831 #[test]
1832 fn loose_over_indent_does_not_steal_nested_under_indent() {
1833 let content = "- Outer\n - Inner\n\n continuation\n";
1840 let warnings = check(content);
1841 assert_eq!(warnings.len(), 1);
1842 assert_eq!(warnings[0].line, 4);
1843 assert!(warnings[0].message.contains("4 spaces"));
1844 assert!(warnings[0].message.contains("found 3"));
1845 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1846 }
1847
1848 #[test]
1849 fn loose_over_indent_attributes_to_deepest_enclosing_item() {
1850 let content = "- Outer\n - Inner\n\n continuation\n";
1854 let warnings = check(content);
1855 assert_eq!(warnings.len(), 1);
1856 assert_eq!(warnings[0].line, 4);
1857 assert!(warnings[0].message.contains("expected 4"));
1858 assert!(warnings[0].message.contains("found 5"));
1859 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1860 }
1861
1862 #[test]
1871 fn loose_over_indented_fence_not_flagged() {
1872 let content = "- Item\n\n ```\n code\n ```\n";
1873 assert!(check(content).is_empty());
1874 assert_eq!(fix(content), content);
1875 }
1876
1877 #[test]
1878 fn tight_over_indented_fence_not_flagged() {
1879 let content = "- Item\n ```\n code\n ```\n";
1880 assert!(check(content).is_empty());
1881 assert_eq!(fix(content), content);
1882 }
1883
1884 #[test]
1885 fn over_indented_tilde_fence_not_flagged() {
1886 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1887 assert!(check(content).is_empty());
1888 assert_eq!(fix(content), content);
1889 }
1890
1891 #[test]
1892 fn fence_like_code_content_inside_fenced_block_not_flagged() {
1893 let content = "- Item\n\n ~~~\n ```\n ~~~\n";
1896 assert!(check(content).is_empty());
1897 assert_eq!(fix(content), content);
1898 }
1899
1900 #[test]
1901 fn unterminated_over_indented_fence_not_flagged() {
1902 let content = "- Item\n\n ```\n code1\n code2deeper\n";
1905 assert!(check(content).is_empty());
1906 assert_eq!(fix(content), content);
1907 }
1908
1909 #[test]
1917 fn task_list_tight_continuation_post_checkbox_reproducer_579() {
1918 let content = "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n";
1921 assert!(check(content).is_empty());
1922 }
1923
1924 #[test]
1925 fn task_list_tight_continuation_dash_unchecked() {
1926 let content = "- [ ] Task\n continuation\n";
1927 assert!(check(content).is_empty());
1928 }
1929
1930 #[test]
1931 fn task_list_tight_continuation_dash_checked_lower() {
1932 let content = "- [x] Task\n continuation\n";
1933 assert!(check(content).is_empty());
1934 }
1935
1936 #[test]
1937 fn task_list_tight_continuation_dash_checked_upper() {
1938 let content = "- [X] Task\n continuation\n";
1939 assert!(check(content).is_empty());
1940 }
1941
1942 #[test]
1943 fn task_list_tight_continuation_star_marker() {
1944 let content = "* [ ] Task\n continuation\n";
1945 assert!(check(content).is_empty());
1946 }
1947
1948 #[test]
1949 fn task_list_tight_continuation_plus_marker() {
1950 let content = "+ [ ] Task\n continuation\n";
1951 assert!(check(content).is_empty());
1952 }
1953
1954 #[test]
1955 fn task_list_tight_continuation_content_column_still_valid() {
1956 let content = "- [ ] Task\n continuation\n";
1959 assert!(check(content).is_empty());
1960 }
1961
1962 #[test]
1963 fn task_list_tight_continuation_between_columns_still_flagged() {
1964 let content = "- [ ] Task\n continuation\n";
1967 let warnings = check(content);
1968 assert_eq!(warnings.len(), 1);
1969 assert!(warnings[0].message.contains("expected 2 or 6"));
1971 assert!(warnings[0].message.contains("found 4"));
1972 }
1973
1974 #[test]
1975 fn task_list_tight_continuation_overshoot_still_flagged() {
1976 let content = "- [ ] Task\n continuation\n";
1978 let warnings = check(content);
1979 assert_eq!(warnings.len(), 1);
1980 assert!(warnings[0].message.contains("expected 2 or 6"));
1981 assert!(warnings[0].message.contains("found 7"));
1982 }
1983
1984 #[test]
1987 fn fix_task_list_overshoot_snaps_to_task_col() {
1988 let content = "- [ ] Task\n continuation\n";
1992 let fixed = fix(content);
1993 assert_eq!(fixed, "- [ ] Task\n continuation\n");
1994 }
1995
1996 #[test]
1997 fn fix_task_list_col_5_snaps_to_task_col() {
1998 let content = "- [ ] Task\n continuation\n";
2000 let fixed = fix(content);
2001 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2002 }
2003
2004 #[test]
2005 fn fix_task_list_col_3_snaps_to_content_col() {
2006 let content = "- [ ] Task\n continuation\n";
2008 let fixed = fix(content);
2009 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2010 }
2011
2012 #[test]
2013 fn fix_task_list_col_4_ties_to_content_col() {
2014 let content = "- [ ] Task\n continuation\n";
2019 let fixed = fix(content);
2020 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2021 }
2022
2023 #[test]
2024 fn fix_task_list_ordered_overshoot_snaps_to_task_col() {
2025 let content = "1. [ ] Task\n continuation\n";
2028 let fixed = fix(content);
2029 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2030 }
2031
2032 #[test]
2033 fn fix_task_list_ordered_under_overshoot_snaps_to_content_col() {
2034 let content = "1. [ ] Task\n continuation\n";
2037 let fixed = fix(content);
2038 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2039 }
2040
2041 #[test]
2042 fn task_list_tight_continuation_ordered_single_digit() {
2043 let content = "1. [ ] Task\n continuation\n";
2045 assert!(check(content).is_empty());
2046 }
2047
2048 #[test]
2049 fn task_list_tight_continuation_ordered_multi_digit() {
2050 let content = "10. [ ] Task\n continuation\n";
2052 assert!(check(content).is_empty());
2053 }
2054
2055 #[test]
2056 fn task_list_tight_continuation_nested_dash() {
2057 let content = "- Parent\n - [ ] Nested task\n continuation\n";
2059 assert!(check(content).is_empty());
2060 }
2061
2062 #[test]
2063 fn task_list_loose_continuation_post_checkbox_column_not_flagged() {
2064 let content = "- [ ] Task\n\n continuation\n";
2069 assert!(check(content).is_empty());
2070 }
2071
2072 #[test]
2073 fn task_list_empty_body_is_not_a_task() {
2074 let content = "- [ ]\n continuation\n";
2080 let warnings = check(content);
2081 assert_eq!(warnings.len(), 1);
2082 assert!(warnings[0].message.contains("found 4"));
2083 }
2084
2085 #[test]
2086 fn task_list_malformed_checkbox_is_not_a_task() {
2087 let content = "- [~] Not a task\n continuation\n";
2089 let warnings = check(content);
2090 assert_eq!(warnings.len(), 1);
2091 }
2092
2093 #[test]
2100 fn task_list_mkdocs_unordered_required_min_valid() {
2101 let content = "- [ ] Task\n continuation\n";
2103 assert!(check_mkdocs(content).is_empty());
2104 }
2105
2106 #[test]
2107 fn task_list_mkdocs_unordered_post_checkbox_valid() {
2108 let content = "- [ ] Task\n continuation\n";
2109 assert!(check_mkdocs(content).is_empty());
2110 }
2111
2112 #[test]
2113 fn task_list_mkdocs_unordered_between_flagged() {
2114 let content = "- [ ] Task\n continuation\n";
2116 let warnings = check_mkdocs(content);
2117 assert_eq!(warnings.len(), 1);
2118 }
2119
2120 #[test]
2121 fn task_list_mkdocs_ordered_both_columns_valid() {
2122 let at_4 = "1. [ ] Task\n continuation\n";
2124 assert!(check_mkdocs(at_4).is_empty());
2125 let at_7 = "1. [ ] Task\n continuation\n";
2126 assert!(check_mkdocs(at_7).is_empty());
2127 }
2128
2129 #[test]
2130 fn task_list_mkdocs_ordered_between_flagged() {
2131 let at_5 = "1. [ ] Task\n continuation\n";
2133 assert_eq!(check_mkdocs(at_5).len(), 1);
2134 let at_6 = "1. [ ] Task\n continuation\n";
2135 assert_eq!(check_mkdocs(at_6).len(), 1);
2136 }
2137
2138 #[test]
2148 fn fix_task_list_tie_sibling_at_task_col_snaps_to_task_col() {
2149 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
2153 let fixed = fix(content);
2154 assert_eq!(
2155 fixed,
2156 "- [ ] Task\n aligned continuation\n tied continuation\n"
2157 );
2158 }
2159
2160 #[test]
2161 fn fix_task_list_tie_sibling_at_content_col_snaps_to_content_col() {
2162 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
2165 let fixed = fix(content);
2166 assert_eq!(fixed, "- [ ] Task\n aligned continuation\n tied continuation\n");
2167 }
2168
2169 #[test]
2170 fn fix_task_list_tie_both_siblings_snaps_to_content_col() {
2171 let content = "- [ ] Task\n at content col\n at task col\n tied continuation\n";
2175 let fixed = fix(content);
2176 assert_eq!(
2177 fixed,
2178 "- [ ] Task\n at content col\n at task col\n tied continuation\n"
2179 );
2180 }
2181
2182 #[test]
2183 fn fix_task_list_tie_sees_task_col_through_tight_lazy_continuation() {
2184 let content = concat!("- [ ] Task\n", "lazy\n", " aligned at task col\n", " tied\n",);
2198 let fixed = fix(content);
2199 assert!(
2200 fixed.contains("\n tied\n"),
2201 "tied line should snap to col 6 (task col) because a task-col \
2202 sibling is visible past the tight lazy-continuation line; got:\n{fixed}"
2203 );
2204 }
2205
2206 #[test]
2213 fn task_list_tab_indented_continuation_flagged() {
2214 let content = "- [ ] Task\n\t\twrap\n";
2217 let warnings = check(content);
2218 assert_eq!(warnings.len(), 1);
2219 assert!(warnings[0].message.contains("expected 2 or 6"));
2220 assert!(warnings[0].message.contains("found 8"));
2221 }
2222
2223 #[test]
2224 fn fix_task_list_tab_indented_snaps_to_task_col() {
2225 let content = "- [ ] Task\n\t\twrap\n";
2227 let fixed = fix(content);
2228 assert_eq!(fixed, "- [ ] Task\n wrap\n");
2229 }
2230
2231 #[test]
2232 fn fix_task_list_single_tab_equidistant_snaps_to_content_col() {
2233 let content = "- [ ] Task\n\twrap\n";
2236 let fixed = fix(content);
2237 assert_eq!(fixed, "- [ ] Task\n wrap\n");
2238 }
2239
2240 #[test]
2250 fn task_list_blockquote_post_checkbox_not_flagged() {
2251 let content = "> - [ ] Task\n> continuation\n";
2253 assert!(check(content).is_empty());
2254 }
2255
2256 #[test]
2257 fn task_list_blockquote_between_cols_documented_limitation() {
2258 let content = "> - [ ] Task\n> continuation\n";
2262 assert!(check(content).is_empty());
2263 }
2264
2265 #[test]
2266 fn task_list_blockquote_overshoot_documented_limitation() {
2267 let content = "> - [ ] Task\n> continuation\n";
2269 assert!(check(content).is_empty());
2270 }
2271
2272 #[test]
2279 fn fix_task_list_mkdocs_unordered_overshoot_snaps_to_task_col() {
2280 let content = "- [ ] Task\n continuation\n";
2283 let fixed = fix_mkdocs(content);
2284 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2285 }
2286
2287 #[test]
2288 fn fix_task_list_mkdocs_unordered_tie_snaps_to_required() {
2289 let content = "- [ ] Task\n continuation\n";
2292 let fixed = fix_mkdocs(content);
2293 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2294 }
2295
2296 #[test]
2297 fn fix_task_list_mkdocs_ordered_overshoot_snaps_to_task_col() {
2298 let content = "1. [ ] Task\n continuation\n";
2301 let fixed = fix_mkdocs(content);
2302 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2303 }
2304
2305 #[test]
2306 fn fix_task_list_mkdocs_ordered_near_required_snaps_to_required() {
2307 let content = "1. [ ] Task\n continuation\n";
2313 let fixed = fix_mkdocs(content);
2314 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2315 }
2316
2317 #[test]
2318 fn fix_task_list_mkdocs_ordered_between_cols_snaps_to_task_col() {
2319 let content = "1. [ ] Task\n continuation\n";
2322 let fixed = fix_mkdocs(content);
2323 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2324 }
2325
2326 fn assert_idempotent(content: &str) {
2336 let once = fix(content);
2337 let twice = fix(&once);
2338 assert_eq!(once, twice, "MD077 fix was not idempotent on input: {content:?}");
2339 }
2340
2341 fn assert_idempotent_mkdocs(content: &str) {
2342 let once = fix_mkdocs(content);
2343 let twice = fix_mkdocs(&once);
2344 assert_eq!(
2345 once, twice,
2346 "MD077 (MkDocs) fix was not idempotent on input: {content:?}"
2347 );
2348 }
2349
2350 #[test]
2351 fn idempotent_task_list_between_cols() {
2352 assert_idempotent("- [ ] Task\n continuation\n");
2353 }
2354
2355 #[test]
2356 fn idempotent_task_list_overshoot() {
2357 assert_idempotent("- [ ] Task\n continuation\n");
2358 }
2359
2360 #[test]
2361 fn idempotent_task_list_under_post_checkbox() {
2362 assert_idempotent("- [ ] Task\n continuation\n");
2363 }
2364
2365 #[test]
2366 fn idempotent_task_list_near_post_checkbox() {
2367 assert_idempotent("- [ ] Task\n continuation\n");
2368 }
2369
2370 #[test]
2371 fn idempotent_task_list_tab_overshoot() {
2372 assert_idempotent("- [ ] Task\n\t\twrap\n");
2373 }
2374
2375 #[test]
2376 fn idempotent_task_list_single_tab() {
2377 assert_idempotent("- [ ] Task\n\twrap\n");
2378 }
2379
2380 #[test]
2381 fn idempotent_task_list_ordered_overshoot() {
2382 assert_idempotent("1. [ ] Task\n continuation\n");
2383 }
2384
2385 #[test]
2386 fn idempotent_task_list_ordered_under() {
2387 assert_idempotent("1. [ ] Task\n continuation\n");
2388 }
2389
2390 #[test]
2391 fn idempotent_task_list_tie_with_sibling_at_task_col() {
2392 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2393 }
2394
2395 #[test]
2396 fn idempotent_task_list_tie_with_sibling_at_content_col() {
2397 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2398 }
2399
2400 #[test]
2401 fn idempotent_task_list_mkdocs_unordered_overshoot() {
2402 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2403 }
2404
2405 #[test]
2406 fn idempotent_task_list_mkdocs_unordered_tie() {
2407 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2408 }
2409
2410 #[test]
2411 fn idempotent_task_list_mkdocs_ordered_overshoot() {
2412 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2413 }
2414
2415 #[test]
2416 fn idempotent_task_list_mkdocs_ordered_between() {
2417 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2418 }
2419
2420 #[test]
2421 fn idempotent_task_list_reproducer_579() {
2422 assert_idempotent(
2426 "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n",
2427 );
2428 }
2429
2430 #[test]
2431 fn idempotent_non_task_list_still_holds() {
2432 assert_idempotent("1. Item\n over-indented\n");
2435 assert_idempotent("- Item\n\n continuation\n");
2436 }
2437
2438 #[test]
2445 fn idempotent_non_task_loose_under_indent_ordered() {
2446 assert_idempotent("1. Item\n\n continuation\n");
2448 }
2449
2450 #[test]
2451 fn idempotent_non_task_loose_under_indent_multi_digit() {
2452 assert_idempotent("10. Item\n\n continuation\n");
2454 }
2455
2456 #[test]
2457 fn idempotent_non_task_tight_over_indent_ordered() {
2458 assert_idempotent("1. Item\n over-indented\n");
2460 }
2461
2462 #[test]
2470 fn idempotent_non_task_fence_ordered_loose() {
2471 assert_idempotent("1. Item\n\n ```rust\n let x = 1;\n ```\n");
2473 }
2474
2475 #[test]
2476 fn idempotent_non_task_fence_tilde_under_indent() {
2477 assert_idempotent("1. Item\n\n ~~~\nplain text\n ~~~\n");
2483 }
2484
2485 #[test]
2486 fn idempotent_non_task_fence_interior_above_required() {
2487 assert_idempotent("1. Item\n\n ```\n deeply indented code\n ```\n");
2491 }
2492
2493 #[test]
2494 fn fence_fix_promotes_interior_below_scope_in_single_pass() {
2495 let content = "1. Item\n\n ```\ncode\n ```\n";
2499 let fixed = fix(content);
2500 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2501 }
2502
2503 #[test]
2504 fn fence_fix_preserves_interior_above_required() {
2505 let content = "1. Item\n\n ```\n code\n ```\n";
2508 let fixed = fix(content);
2509 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2510 }
2511
2512 #[test]
2519 fn idempotent_non_task_mkdocs_ordered_at_3_spaces() {
2520 assert_idempotent_mkdocs("1. Item\n\n continuation\n");
2522 }
2523
2524 #[test]
2525 fn idempotent_non_task_mkdocs_unordered_at_2_spaces() {
2526 assert_idempotent_mkdocs("- Item\n\n continuation\n");
2528 }
2529
2530 #[test]
2531 fn idempotent_non_task_mkdocs_fence_compound() {
2532 assert_idempotent_mkdocs("1. Item\n\n ```toml\n k = 1\n ```\n");
2534 }
2535
2536 #[test]
2539 fn aligned_tight_zero_indent_continuation_flagged() {
2540 let content = "- this is a long line\nthat continues on a second line\n";
2544 let warnings = check_aligned(content);
2545 assert_eq!(warnings.len(), 1);
2546 assert_eq!(warnings[0].line, 2);
2547 assert_eq!(
2548 fix_aligned(content),
2549 "- this is a long line\n that continues on a second line\n"
2550 );
2551 }
2552
2553 #[test]
2554 fn aligned_full_issue_example_made_consistent() {
2555 let content = "- this is a long line\n\
2558 that continues on a second line\n\
2559 - this is another long line\n\
2560 \x20\x20that continues on the next line\n\
2561 - yet again a long line\n\
2562 and still inconsistently spaced\n\
2563 \x20\x20and even worse\n";
2564 let expected = "- this is a long line\n\
2565 \x20\x20that continues on a second line\n\
2566 - this is another long line\n\
2567 \x20\x20that continues on the next line\n\
2568 - yet again a long line\n\
2569 \x20\x20and still inconsistently spaced\n\
2570 \x20\x20and even worse\n";
2571 assert_eq!(fix_aligned(content), expected);
2572 assert_eq!(fix_aligned(expected), expected);
2574 }
2575
2576 #[test]
2577 fn aligned_already_aligned_not_flagged() {
2578 let content = "- item\n continuation at content column\n";
2579 assert!(check_aligned(content).is_empty());
2580 }
2581
2582 #[test]
2583 fn aligned_tight_partial_indent_flagged() {
2584 let content = "- item\n continuation\n";
2586 let warnings = check_aligned(content);
2587 assert_eq!(warnings.len(), 1);
2588 assert_eq!(fix_aligned(content), "- item\n continuation\n");
2589 }
2590
2591 #[test]
2592 fn aligned_post_blank_zero_indent_still_new_paragraph() {
2593 let content = "- item\n\nnew paragraph\n";
2596 assert!(check_aligned(content).is_empty());
2597 assert_eq!(fix_aligned(content), content);
2598 }
2599
2600 #[test]
2603 fn aligned_top_level_blockquote_after_list_untouched() {
2604 let content = "- item\n> quote\n";
2608 assert!(check_aligned(content).is_empty());
2609 assert_eq!(fix_aligned(content), content);
2610 }
2611
2612 #[test]
2613 fn aligned_top_level_fence_after_list_untouched() {
2614 let content = "- item\n```\ncode\n```\n";
2615 assert!(check_aligned(content).is_empty());
2616 assert_eq!(fix_aligned(content), content);
2617 }
2618
2619 #[test]
2620 fn aligned_top_level_table_after_list_untouched() {
2621 let content = "- item\n| a | b |\n|---|---|\n| 1 | 2 |\n";
2622 assert!(check_aligned(content).is_empty());
2623 assert_eq!(fix_aligned(content), content);
2624 }
2625
2626 #[test]
2629 fn aligned_nested_tight_lazy_continuation_aligns_to_inner() {
2630 let content = "- Outer\n - Inner\ncontinuation\n";
2635 let warnings = check_aligned(content);
2636 assert_eq!(warnings.len(), 1);
2637 assert_eq!(fix_aligned(content), "- Outer\n - Inner\n continuation\n");
2638 }
2639
2640 #[test]
2641 fn aligned_nested_continuation_already_aligned_not_flagged() {
2642 let content = "- L1\n - L2\n cont of L2 at 4\n";
2643 assert!(check_aligned(content).is_empty());
2644 }
2645
2646 #[test]
2647 fn aligned_nested_idempotent() {
2648 let content = "- Outer\n - Inner\ncontinuation\n";
2649 let once = fix_aligned(content);
2650 assert_eq!(fix_aligned(&once), once);
2651 }
2652
2653 #[test]
2654 fn aligned_three_level_nesting_aligns_to_innermost() {
2655 let content = "- L1\n - L2\n - L3\ncont\n";
2658 assert_eq!(fix_aligned(content), "- L1\n - L2\n - L3\n cont\n");
2659 }
2660
2661 #[test]
2662 fn aligned_continuation_after_sibling_owned_by_last_item() {
2663 let content = "- a\n- b\nlazy\n";
2666 assert_eq!(fix_aligned(content), "- a\n- b\n lazy\n");
2667 }
2668
2669 #[test]
2670 fn aligned_multi_digit_ordered_marker_aligns_to_content_column() {
2671 let content = "10. Item\nwrap\n";
2672 assert_eq!(fix_aligned(content), "10. Item\n wrap\n");
2673 }
2674
2675 #[test]
2676 fn aligned_setext_heading_after_list_left_alone() {
2677 let content = "- item\nText\n===\n";
2680 assert!(check_aligned(content).is_empty());
2681 assert_eq!(fix_aligned(content), content);
2682 }
2683
2684 #[test]
2685 fn aligned_latent_marker_in_continuation_is_idempotent() {
2686 let content = "# \n- \n``\n2. \n![]()";
2692 let once = fix_aligned(content);
2693 assert_eq!(fix_aligned(&once), once, "fix must be idempotent in one pass");
2694 assert_eq!(once, content, "item with a latent marker is left untouched");
2695 }
2696
2697 #[test]
2698 fn aligned_latent_table_in_continuation_is_idempotent() {
2699 let content = "- \n![`]()\n| | ` |\n| --- | --- |";
2704 let once = fix_aligned(content);
2705 assert_eq!(fix_aligned(&once), once, "fix must be idempotent in one pass");
2706 assert_eq!(once, content, "item with a latent table is left untouched");
2707 }
2708
2709 #[test]
2710 fn aligned_blockquote_nested_list_not_touched() {
2711 let content = "> - item\n> wrap\n";
2715 assert!(check_aligned(content).is_empty());
2716 assert_eq!(fix_aligned(content), content);
2717 }
2718
2719 #[test]
2722 fn aligned_task_post_checkbox_column_accepted() {
2723 let content = "- [ ] Task\n wrap\n";
2726 assert!(check_aligned(content).is_empty());
2727 assert_eq!(fix_aligned(content), content);
2728 }
2729
2730 #[test]
2731 fn aligned_task_under_indent_snaps_to_content_column() {
2732 let content = "- [ ] Task\nwrap\n";
2733 let warnings = check_aligned(content);
2734 assert_eq!(warnings.len(), 1);
2735 assert_eq!(fix_aligned(content), "- [ ] Task\n wrap\n");
2736 }
2737
2738 #[test]
2741 fn aligned_mkdocs_tight_under_indent_snaps_to_four() {
2742 let content = "- item\nwrap\n";
2744 let warnings = check_aligned_mkdocs(content);
2745 assert_eq!(warnings.len(), 1);
2746 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
2747 assert_eq!(aligned_rule().fix(&ctx).unwrap(), "- item\n wrap\n");
2748 }
2749
2750 #[test]
2753 fn any_default_does_not_flag_tight_lazy_continuation() {
2754 let content = "- item\nwrapped at zero indent\n";
2756 assert!(check(content).is_empty());
2757 assert_eq!(fix(content), content);
2758 }
2759
2760 #[test]
2761 fn from_config_aligned_enables_tight_flagging() {
2762 let mut config = crate::config::Config::default();
2764 let mut rule_config = crate::config::RuleConfig::default();
2765 rule_config
2766 .values
2767 .insert("style".to_string(), toml::Value::String("aligned".to_string()));
2768 config.rules.insert("MD077".to_string(), rule_config);
2769
2770 let rule = MD077ListContinuationIndent::from_config(&config);
2771 let ctx = LintContext::new("- item\nwrap\n", MarkdownFlavor::Standard, None);
2772 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
2773 }
2774
2775 #[test]
2776 fn from_config_default_is_any() {
2777 let config = crate::config::Config::default();
2779 let rule = MD077ListContinuationIndent::from_config(&config);
2780 let ctx = LintContext::new("- item\nwrap\n", MarkdownFlavor::Standard, None);
2781 assert!(rule.check(&ctx).unwrap().is_empty());
2782 }
2783
2784 #[test]
2785 fn aligned_tight_underindented_fence_inside_item_left_alone() {
2786 let content = "- item\n ```\n code\n ```\n";
2790 assert!(check_aligned(content).is_empty());
2791 assert_eq!(fix_aligned(content), content);
2792 }
2793
2794 #[test]
2795 fn aligned_task_under_indent_fix_is_idempotent() {
2796 let content = "- [ ] Task\nwrap\n";
2797 let once = fix_aligned(content);
2798 assert_eq!(fix_aligned(&once), once);
2799 }
2800
2801 #[test]
2802 fn aligned_partial_indent_fix_is_idempotent() {
2803 let content = "- item\n continuation\n";
2804 let once = fix_aligned(content);
2805 assert_eq!(fix_aligned(&once), once);
2806 }
2807}