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>(
252 ctx: &LintContext,
253 item_line: usize,
254 range_end: usize,
255 marker_col: usize,
256 mut per_line: F,
257 ) where
258 F: FnMut(&ContinuationLine<'_>) -> ControlFlow<()>,
259 {
260 let mut saw_blank = false;
261 let mut saw_nested = false;
262 let mut nested_stack: Vec<(usize, usize)> = Vec::new();
268
269 for line_num in (item_line + 1)..=range_end {
270 let Some(info) = ctx.line_info(line_num) else {
271 continue;
272 };
273
274 let trimmed = info.content(ctx.content).trim_start();
275
276 if Self::should_skip_line(info, trimmed) {
277 continue;
278 }
279
280 if info.is_blank {
281 saw_blank = true;
282 continue;
283 }
284
285 if let Some(ref li) = info.list_item {
286 if li.marker_column > marker_col {
287 while nested_stack.last().is_some_and(|&(m, _)| m >= li.marker_column) {
290 nested_stack.pop();
291 }
292 nested_stack.push((li.marker_column, li.content_column));
293 saw_nested = true;
298 } else {
299 nested_stack.clear();
300 }
301 saw_blank = false;
302 continue;
303 }
304
305 if info.heading.is_some() || info.is_horizontal_rule {
306 break;
307 }
308
309 if Self::is_block_level_construct(trimmed) {
310 continue;
311 }
312
313 let col = info.visual_indent;
314
315 while nested_stack.last().is_some_and(|&(_, c)| c > col) {
319 nested_stack.pop();
320 }
321 if !nested_stack.is_empty() {
322 continue;
323 }
324
325 if saw_blank && col <= marker_col {
326 break;
327 }
328
329 let line = ContinuationLine {
330 line_num,
331 info,
332 trimmed,
333 actual: col,
334 saw_blank,
335 saw_nested,
336 };
337 if per_line(&line).is_break() {
338 break;
339 }
340 }
341 }
342
343 fn item_range_has_latent_structure(ctx: &LintContext, item_line: usize, range_end: usize) -> bool {
359 (item_line + 1..=range_end).any(|line_num| {
360 ctx.line_info(line_num).is_some_and(|info| {
361 if info.is_blank || info.list_item.is_some() {
362 return false;
363 }
364 let trimmed = info.content(ctx.content).trim_start();
365 !Self::should_skip_line(info, trimmed)
366 && (Self::starts_with_list_marker(trimmed) || crate::utils::skip_context::is_table_line(trimmed))
367 })
368 })
369 }
370
371 fn sibling_column_usage(
381 ctx: &LintContext,
382 item_line: usize,
383 range_end: usize,
384 marker_col: usize,
385 content_col: usize,
386 task_col: usize,
387 ) -> (bool, bool) {
388 let mut uses_content = false;
389 let mut uses_task = false;
390
391 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
392 if line.actual == content_col {
393 uses_content = true;
394 }
395 if line.actual == task_col {
396 uses_task = true;
397 }
398 if uses_content && uses_task {
399 ControlFlow::Break(())
400 } else {
401 ControlFlow::Continue(())
402 }
403 });
404
405 (uses_content, uses_task)
406 }
407
408 fn compute_fix_target(
414 actual: usize,
415 required: usize,
416 task_col: Option<usize>,
417 uses_content_col: bool,
418 uses_task_col: bool,
419 ) -> usize {
420 let Some(t) = task_col else { return required };
421 match actual.abs_diff(t).cmp(&actual.abs_diff(required)) {
422 std::cmp::Ordering::Less => t,
423 std::cmp::Ordering::Greater => required,
424 std::cmp::Ordering::Equal => match (uses_task_col, uses_content_col) {
425 (true, false) => t,
426 _ => required,
427 },
428 }
429 }
430
431 fn should_skip_line(info: &crate::lint_context::LineInfo, trimmed: &str) -> bool {
442 if info.in_code_block && !Self::is_code_fence(trimmed) {
443 return true;
444 }
445 info.in_front_matter
446 || info.in_footnote_definition
447 || info.in_html_block
448 || info.in_html_comment
449 || info.in_mdx_comment
450 || info.in_mkdocstrings
451 || info.in_esm_block
452 || info.in_math_block
453 || info.in_admonition
454 || info.in_content_tab
455 || info.in_pymdown_block
456 || info.in_definition_list
457 || info.in_mkdocs_html_markdown
458 || info.in_kramdown_extension_block
459 }
460
461 fn build_over_indent_warning(
470 ctx: &LintContext,
471 line: &ContinuationLine<'_>,
472 fix_target: usize,
473 message: String,
474 ) -> LintWarning {
475 let line_content = line.info.content(ctx.content);
476 let fix_start = line.info.byte_offset;
477 let fix_end = fix_start + line.info.indent;
478 LintWarning {
479 rule_name: Some("MD077".to_string()),
480 line: line.line_num,
481 column: 1,
482 end_line: line.line_num,
483 end_column: line_content.chars().count() + 1,
484 message,
485 severity: Severity::Warning,
486 fix: Some(Fix::new(fix_start..fix_end, " ".repeat(fix_target))),
487 }
488 }
489
490 fn build_under_indent_warning(
502 ctx: &LintContext,
503 line: &ContinuationLine<'_>,
504 required: usize,
505 message: String,
506 ) -> UnderIndentOutcome {
507 let line_content = line.info.content(ctx.content);
508 let is_fence_opener = line.info.in_code_block
509 && Self::is_code_fence(line.trimmed)
510 && ctx.line_info(line.line_num - 1).is_none_or(|p| !p.in_code_block);
511
512 let (fix, warn_end_line, warn_end_column, compound_closer) = if is_fence_opener {
513 let closer_line = Self::find_fence_closer(ctx, line.line_num);
514 let fix = Self::build_compound_fence_fix(ctx, line.line_num, closer_line, line.actual, required);
515 let end_column = ctx
516 .line_info(closer_line)
517 .map_or(line_content.chars().count() + 1, |ci| {
518 ci.content(ctx.content).chars().count() + 1
519 });
520 let extra_flag = (closer_line != line.line_num).then_some(closer_line);
521 (fix, closer_line, end_column, extra_flag)
522 } else {
523 let fix_start = line.info.byte_offset;
524 let fix_end = fix_start + line.info.indent;
525 let fix = Some(Fix::new(fix_start..fix_end, " ".repeat(required)));
526 (fix, line.line_num, line_content.chars().count() + 1, None)
527 };
528
529 UnderIndentOutcome {
530 warning: LintWarning {
531 rule_name: Some("MD077".to_string()),
532 line: line.line_num,
533 column: 1,
534 end_line: warn_end_line,
535 end_column: warn_end_column,
536 message,
537 severity: Severity::Warning,
538 fix,
539 },
540 also_flag_line: compound_closer,
541 }
542 }
543}
544
545struct ContinuationLine<'a> {
549 line_num: usize,
550 info: &'a LineInfo,
551 trimmed: &'a str,
552 actual: usize,
553 saw_blank: bool,
554 saw_nested: bool,
558}
559
560struct UnderIndentOutcome {
565 warning: LintWarning,
566 also_flag_line: Option<usize>,
567}
568
569impl Rule for MD077ListContinuationIndent {
570 fn name(&self) -> &'static str {
571 "MD077"
572 }
573
574 fn description(&self) -> &'static str {
575 "List continuation content indentation"
576 }
577
578 fn check(&self, ctx: &LintContext) -> LintResult {
579 if ctx.content.is_empty() {
580 return Ok(Vec::new());
581 }
582
583 let strict_indent = ctx.flavor.requires_strict_list_indent();
584 let total_lines = ctx.lines.len();
585 let mut warnings = Vec::new();
586 let mut flagged_lines = std::collections::HashSet::new();
587
588 let mut items: Vec<(usize, usize, usize, Option<usize>)> = Vec::new();
597 for block in &ctx.list_blocks {
598 for &item_line in &block.item_lines {
599 if let Some(info) = ctx.line_info(item_line)
600 && let Some(ref li) = info.list_item
601 {
602 let line = info.content(ctx.content);
603 let task_col = Self::is_task_list_item(line, li.content_column)
604 .then_some(li.content_column + Self::TASK_CHECKBOX_PREFIX_LEN);
605 items.push((item_line, li.marker_column, li.content_column, task_col));
606 }
607 }
608 }
609 items.sort_unstable();
610 items.dedup_by_key(|&mut (ln, _, _, _)| ln);
611
612 let mut range_ends = vec![total_lines; items.len()];
625 let mut stack: Vec<usize> = Vec::new();
626 for i in (0..items.len()).rev() {
627 let marker_col = items[i].1;
628 while let Some(&top) = stack.last() {
629 if items[top].1 > marker_col {
630 stack.pop();
631 } else {
632 break;
633 }
634 }
635 range_ends[i] = stack.last().map_or(total_lines, |&j| items[j].0 - 1);
636 stack.push(i);
637 }
638
639 let scoped: Vec<(usize, usize, usize, Option<usize>, usize, usize)> = items
642 .iter()
643 .enumerate()
644 .map(|(item_idx, &(item_line, marker_col, content_col, task_col))| {
645 let required = if strict_indent { content_col.max(4) } else { content_col };
646 (
647 item_line,
648 marker_col,
649 content_col,
650 task_col,
651 required,
652 range_ends[item_idx],
653 )
654 })
655 .collect();
656
657 let prose_candidate_lines: Vec<usize> = (1..=total_lines)
671 .filter(|&line_num| {
672 let Some(info) = ctx.line_info(line_num) else {
673 return false;
674 };
675 let trimmed = info.content(ctx.content).trim_start();
676 !Self::should_skip_line(info, trimmed)
677 && !info.is_blank
678 && info.list_item.is_none()
679 && info.heading.is_none()
680 && !info.is_horizontal_rule
681 && !Self::is_block_level_construct(trimmed)
682 })
683 .collect();
684 let range_has_prose_candidate = |after_line: usize, range_end: usize| -> bool {
687 let start = prose_candidate_lines.partition_point(|&l| l <= after_line);
688 prose_candidate_lines.get(start).is_some_and(|&l| l <= range_end)
689 };
690
691 let aligned = self.config.style == ContinuationStyle::Aligned;
717 for &(item_line, marker_col, _content_col, _task_col, required, range_end) in &scoped {
718 if !range_has_prose_candidate(item_line, range_end) {
721 continue;
722 }
723 let has_latent_structure = aligned && Self::item_range_has_latent_structure(ctx, item_line, range_end);
738 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
739 let actual = line.actual;
740 let under_indented = actual < required;
741 let loose_escape = line.saw_blank && under_indented;
742 let confirmed_structure = line.info.in_code_block || line.info.blockquote.is_some();
749 let aligned_tight = aligned
750 && !has_latent_structure
751 && !line.saw_blank
752 && !line.saw_nested
753 && under_indented
754 && !confirmed_structure;
755 if (loose_escape || aligned_tight) && flagged_lines.insert(line.line_num) {
756 let message = if line.saw_blank {
757 if strict_indent {
758 format!(
759 "Content inside list item needs {required} spaces of indentation \
760 for MkDocs compatibility (found {actual})",
761 )
762 } else {
763 format!(
764 "Content after blank line in list item needs {required} spaces of \
765 indentation to remain part of the list (found {actual})",
766 )
767 }
768 } else {
769 format!("Continuation line under-indented (expected {required}, found {actual})")
770 };
771 let outcome = Self::build_under_indent_warning(ctx, line, required, message);
772 if let Some(closer_line) = outcome.also_flag_line {
773 flagged_lines.insert(closer_line);
774 }
775 warnings.push(outcome.warning);
776 }
777 ControlFlow::Continue(())
778 });
779 }
780
781 for &(item_line, marker_col, content_col, task_col, required, range_end) in &scoped {
790 if !range_has_prose_candidate(item_line, range_end) {
792 continue;
793 }
794 let (uses_content_col, uses_task_col) = match task_col {
798 Some(t) => Self::sibling_column_usage(ctx, item_line, range_end, marker_col, content_col, t),
799 None => (false, false),
800 };
801
802 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
803 let actual = line.actual;
804 if actual > required
805 && !line.info.in_code_block
806 && Some(actual) != task_col
807 && !Self::starts_with_list_marker(line.trimmed)
808 && flagged_lines.insert(line.line_num)
809 {
810 let fix_target =
811 Self::compute_fix_target(actual, required, task_col, uses_content_col, uses_task_col);
812 let message = match task_col {
813 Some(t) => format!(
814 "Continuation line over-indented \
815 (expected {required} or {t}, found {actual})"
816 ),
817 None => {
818 format!("Continuation line over-indented (expected {required}, found {actual})")
819 }
820 };
821 warnings.push(Self::build_over_indent_warning(ctx, line, fix_target, message));
822 }
823 ControlFlow::Continue(())
824 });
825 }
826
827 warnings.sort_by_key(|w| (w.line, w.column));
830
831 Ok(warnings)
832 }
833
834 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
835 let warnings = self.check(ctx)?;
836 let warnings =
837 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
838 if warnings.is_empty() {
839 return Ok(ctx.content.to_string());
840 }
841
842 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
844 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
845
846 let mut content = ctx.content.to_string();
847 for fix in fixes {
848 if fix.range.start <= content.len() && fix.range.end <= content.len() {
849 content.replace_range(fix.range, &fix.replacement);
850 }
851 }
852
853 Ok(content)
854 }
855
856 fn category(&self) -> RuleCategory {
857 RuleCategory::List
858 }
859
860 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
861 ctx.content.is_empty() || ctx.list_blocks.is_empty()
862 }
863
864 fn as_any(&self) -> &dyn std::any::Any {
865 self
866 }
867
868 crate::impl_rule_config_methods!(MD077Config);
869}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874 use crate::config::MarkdownFlavor;
875
876 fn check(content: &str) -> Vec<LintWarning> {
877 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
878 let rule = MD077ListContinuationIndent::default();
879 rule.check(&ctx).unwrap()
880 }
881
882 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
883 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
884 let rule = MD077ListContinuationIndent::default();
885 rule.check(&ctx).unwrap()
886 }
887
888 fn fix(content: &str) -> String {
889 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
890 let rule = MD077ListContinuationIndent::default();
891 rule.fix(&ctx).unwrap()
892 }
893
894 fn fix_mkdocs(content: &str) -> String {
895 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
896 let rule = MD077ListContinuationIndent::default();
897 rule.fix(&ctx).unwrap()
898 }
899
900 fn aligned_rule() -> MD077ListContinuationIndent {
901 MD077ListContinuationIndent::new(ContinuationStyle::Aligned)
902 }
903
904 fn check_aligned(content: &str) -> Vec<LintWarning> {
905 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
906 aligned_rule().check(&ctx).unwrap()
907 }
908
909 fn check_aligned_mkdocs(content: &str) -> Vec<LintWarning> {
910 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
911 aligned_rule().check(&ctx).unwrap()
912 }
913
914 fn fix_aligned(content: &str) -> String {
915 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
916 aligned_rule().fix(&ctx).unwrap()
917 }
918
919 fn fix_aligned_quarto(content: &str) -> String {
920 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
921 aligned_rule().fix(&ctx).unwrap()
922 }
923
924 #[test]
925 fn aligned_idempotent_with_latent_marker_behind_unstable_heading() {
926 let input = "1. \n``\n``\n- \n``";
935 let once = fix_aligned_quarto(input);
936 let twice = fix_aligned_quarto(&once);
937 assert_eq!(once, twice, "MD077 aligned fix must be idempotent (Quarto)");
938 }
939
940 #[test]
943 fn tight_lazy_continuation_zero_indent_not_flagged() {
944 let content = "- Item\ncontinuation\n";
946 assert!(check(content).is_empty());
947 }
948
949 #[test]
950 fn tight_continuation_correct_indent_not_flagged() {
951 let content = "1. Item\n continuation\n";
953 assert!(check(content).is_empty());
954 }
955
956 #[test]
957 fn tight_continuation_over_indented_ordered() {
958 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
960 let warnings = check(content);
961 assert_eq!(warnings.len(), 1);
962 assert_eq!(warnings[0].line, 2);
963 assert!(warnings[0].message.contains("over-indented"));
964 }
965
966 #[test]
967 fn tight_continuation_over_indented_unordered() {
968 let content = "- Item\n over-indented\n";
970 let warnings = check(content);
971 assert_eq!(warnings.len(), 1);
972 assert_eq!(warnings[0].line, 2);
973 }
974
975 #[test]
976 fn tight_continuation_multiple_over_indented_lines() {
977 let content = "1. Item\n line one\n line two\n line three\n";
978 let warnings = check(content);
979 assert_eq!(warnings.len(), 3);
980 }
981
982 #[test]
983 fn tight_continuation_mixed_correct_and_over() {
984 let content = "1. Item\n correct\n over-indented\n correct again\n";
985 let warnings = check(content);
986 assert_eq!(warnings.len(), 1);
987 assert_eq!(warnings[0].line, 3);
988 }
989
990 #[test]
991 fn tight_continuation_nested_over_indented() {
992 let content = "- L1\n - L2\n over-indented continuation of L2\n";
994 let warnings = check(content);
995 assert_eq!(warnings.len(), 1);
996 assert_eq!(warnings[0].line, 3);
997 assert!(warnings[0].message.contains("expected 4"));
999 assert!(warnings[0].message.contains("found 5"));
1000 }
1001
1002 #[test]
1003 fn tight_continuation_nested_correct_indent_not_flagged() {
1004 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
1007 assert!(check(content).is_empty());
1008 }
1009
1010 #[test]
1011 fn fix_tight_continuation_nested_over_indented() {
1012 let content = "- L1\n - L2\n over-indented continuation of L2\n";
1014 let fixed = fix(content);
1015 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
1016 }
1017
1018 #[test]
1019 fn tight_continuation_under_indented_not_flagged() {
1020 let content = "1. Item\n under-indented\n";
1023 assert!(check(content).is_empty());
1024 }
1025
1026 #[test]
1027 fn tight_continuation_tab_over_indented() {
1028 let content = "- Item\n\tover-indented\n";
1030 let warnings = check(content);
1031 assert_eq!(warnings.len(), 1);
1032 }
1033
1034 #[test]
1035 fn fix_tight_continuation_over_indented_ordered() {
1036 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
1037 let fixed = fix(content);
1038 assert_eq!(
1039 fixed,
1040 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
1041 );
1042 }
1043
1044 #[test]
1045 fn fix_tight_continuation_over_indented_unordered() {
1046 let content = "- Item\n over-indented\n";
1047 let fixed = fix(content);
1048 assert_eq!(fixed, "- Item\n over-indented\n");
1049 }
1050
1051 #[test]
1052 fn fix_tight_continuation_multiple_lines() {
1053 let content = "1. Item\n line one\n line two\n";
1054 let fixed = fix(content);
1055 assert_eq!(fixed, "1. Item\n line one\n line two\n");
1056 }
1057
1058 #[test]
1059 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
1060 let content = "1. Item\n continuation\n";
1063 assert!(check_mkdocs(content).is_empty());
1064 }
1065
1066 #[test]
1067 fn tight_continuation_mkdocs_5space_ordered_flagged() {
1068 let content = "1. Item\n over-indented\n";
1070 let warnings = check_mkdocs(content);
1071 assert_eq!(warnings.len(), 1);
1072 assert!(warnings[0].message.contains("expected 4"));
1073 assert!(warnings[0].message.contains("found 5"));
1074 }
1075
1076 #[test]
1077 fn fix_tight_continuation_mkdocs_over_indented() {
1078 let content = "1. Item\n over-indented\n";
1079 let fixed = fix_mkdocs(content);
1080 assert_eq!(fixed, "1. Item\n over-indented\n");
1081 }
1082
1083 #[test]
1084 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
1085 let content = "* Level 0\n * Level 1\n * Level 2\n";
1088 assert!(check(content).is_empty());
1089 }
1090
1091 #[test]
1092 fn tight_continuation_ordered_marker_not_flagged() {
1093 let content = "- Parent\n 1. Child item\n";
1095 assert!(check(content).is_empty());
1096 }
1097
1098 #[test]
1101 fn unordered_correct_indent_no_warning() {
1102 let content = "- Item\n\n continuation\n";
1103 assert!(check(content).is_empty());
1104 }
1105
1106 #[test]
1107 fn unordered_partial_indent_warns() {
1108 let content = "- Item\n\n continuation\n";
1111 let warnings = check(content);
1112 assert_eq!(warnings.len(), 1);
1113 assert_eq!(warnings[0].line, 3);
1114 assert!(warnings[0].message.contains("2 spaces"));
1115 assert!(warnings[0].message.contains("found 1"));
1116 }
1117
1118 #[test]
1119 fn unordered_zero_indent_is_new_paragraph() {
1120 let content = "- Item\n\ncontinuation\n";
1123 assert!(check(content).is_empty());
1124 }
1125
1126 #[test]
1129 fn ordered_3space_correct_commonmark() {
1130 let content = "1. Item\n\n continuation\n";
1132 assert!(check(content).is_empty());
1133 }
1134
1135 #[test]
1136 fn ordered_2space_under_indent_commonmark() {
1137 let content = "1. Item\n\n continuation\n";
1138 let warnings = check(content);
1139 assert_eq!(warnings.len(), 1);
1140 assert!(warnings[0].message.contains("3 spaces"));
1141 assert!(warnings[0].message.contains("found 2"));
1142 }
1143
1144 #[test]
1147 fn multi_digit_marker_correct() {
1148 let content = "10. Item\n\n continuation\n";
1150 assert!(check(content).is_empty());
1151 }
1152
1153 #[test]
1154 fn multi_digit_marker_under_indent() {
1155 let content = "10. Item\n\n continuation\n";
1156 let warnings = check(content);
1157 assert_eq!(warnings.len(), 1);
1158 assert!(warnings[0].message.contains("4 spaces"));
1159 }
1160
1161 #[test]
1164 fn mkdocs_3space_ordered_warns() {
1165 let content = "1. Item\n\n continuation\n";
1167 let warnings = check_mkdocs(content);
1168 assert_eq!(warnings.len(), 1);
1169 assert!(warnings[0].message.contains("4 spaces"));
1170 assert!(warnings[0].message.contains("MkDocs"));
1171 }
1172
1173 #[test]
1174 fn mkdocs_4space_ordered_no_warning() {
1175 let content = "1. Item\n\n continuation\n";
1176 assert!(check_mkdocs(content).is_empty());
1177 }
1178
1179 #[test]
1180 fn mkdocs_unordered_2space_ok() {
1181 let content = "- Item\n\n continuation\n";
1183 assert!(check_mkdocs(content).is_empty());
1184 }
1185
1186 #[test]
1187 fn mkdocs_unordered_2space_warns() {
1188 let content = "- Item\n\n continuation\n";
1190 let warnings = check_mkdocs(content);
1191 assert_eq!(warnings.len(), 1);
1192 assert!(warnings[0].message.contains("4 spaces"));
1193 }
1194
1195 #[test]
1198 fn fix_unordered_indent() {
1199 let content = "- Item\n\n continuation\n";
1201 let fixed = fix(content);
1202 assert_eq!(fixed, "- Item\n\n continuation\n");
1203 }
1204
1205 #[test]
1206 fn fix_ordered_indent() {
1207 let content = "1. Item\n\n continuation\n";
1208 let fixed = fix(content);
1209 assert_eq!(fixed, "1. Item\n\n continuation\n");
1210 }
1211
1212 #[test]
1213 fn fix_mkdocs_indent() {
1214 let content = "1. Item\n\n continuation\n";
1215 let fixed = fix_mkdocs(content);
1216 assert_eq!(fixed, "1. Item\n\n continuation\n");
1217 }
1218
1219 #[test]
1222 fn nested_list_items_not_flagged() {
1223 let content = "- Parent\n\n - Child\n";
1224 assert!(check(content).is_empty());
1225 }
1226
1227 #[test]
1228 fn nested_list_zero_indent_is_new_paragraph() {
1229 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
1231 assert!(check(content).is_empty());
1232 }
1233
1234 #[test]
1235 fn nested_list_partial_indent_flagged() {
1236 let content = "- Parent\n - Child\n\n continuation of parent\n";
1238 let warnings = check(content);
1239 assert_eq!(warnings.len(), 1);
1240 assert!(warnings[0].message.contains("2 spaces"));
1241 }
1242
1243 #[test]
1246 fn code_block_correctly_indented_no_warning() {
1247 let content = "- Item\n\n ```\n code\n ```\n";
1249 assert!(check(content).is_empty());
1250 }
1251
1252 #[test]
1253 fn code_fence_under_indented_warns() {
1254 let content = "- Item\n\n ```\n code\n ```\n";
1258 let warnings = check(content);
1259 assert_eq!(warnings.len(), 1);
1260 assert_eq!(warnings[0].line, 3);
1261 }
1262
1263 #[test]
1264 fn code_fence_under_indented_ordered_mkdocs() {
1265 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
1268 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
1270 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1272 assert!(warnings[0].message.contains("4 spaces"));
1273 assert!(warnings[0].message.contains("MkDocs"));
1274 }
1275
1276 #[test]
1277 fn code_fence_tilde_under_indented() {
1278 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1279 let warnings = check(content);
1280 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1282 }
1283
1284 #[test]
1287 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
1288 let content = "- Item\n\n\ncontinuation\n";
1290 assert!(check(content).is_empty());
1291 }
1292
1293 #[test]
1294 fn multiple_blank_lines_partial_indent_flags() {
1295 let content = "- Item\n\n\n continuation\n";
1296 let warnings = check(content);
1297 assert_eq!(warnings.len(), 1);
1298 }
1299
1300 #[test]
1303 fn empty_item_no_warning() {
1304 let content = "- \n- Second\n";
1305 assert!(check(content).is_empty());
1306 }
1307
1308 #[test]
1311 fn multiple_items_mixed_indent() {
1312 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
1313 let warnings = check(content);
1314 assert_eq!(warnings.len(), 1);
1315 assert_eq!(warnings[0].line, 7);
1316 }
1317
1318 #[test]
1321 fn task_list_correct_indent() {
1322 let content = "- [ ] Task\n\n continuation\n";
1324 assert!(check(content).is_empty());
1325 }
1326
1327 #[test]
1330 fn frontmatter_not_flagged() {
1331 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
1332 assert!(check(content).is_empty());
1333 }
1334
1335 #[test]
1338 fn fix_multiple_items() {
1339 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
1340 let fixed = fix(content);
1341 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
1342 }
1343
1344 #[test]
1345 fn fix_multiline_loose_continuation_all_lines() {
1346 let content = "1. Item\n\n line one\n line two\n line three\n";
1347 let fixed = fix(content);
1348 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
1349 }
1350
1351 #[test]
1354 fn sibling_item_boundary_respected() {
1355 let content = "- First\n- Second\n\n continuation\n";
1357 assert!(check(content).is_empty());
1358 }
1359
1360 #[test]
1363 fn blockquote_list_correct_indent_no_warning() {
1364 let content = "> - Item\n>\n> continuation\n";
1367 assert!(check(content).is_empty());
1368 }
1369
1370 #[test]
1371 fn blockquote_list_under_indent_no_false_positive() {
1372 let content = "> - Item\n>\n> continuation\n";
1377 assert!(check(content).is_empty());
1378 }
1379
1380 #[test]
1383 fn deep_nesting_correct_indent() {
1384 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1385 assert!(check(content).is_empty());
1386 }
1387
1388 #[test]
1389 fn deep_nesting_under_indent() {
1390 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1393 let warnings = check(content);
1394 assert_eq!(warnings.len(), 1);
1395 assert!(warnings[0].message.contains("6 spaces"));
1396 assert!(warnings[0].message.contains("found 5"));
1397 }
1398
1399 #[test]
1400 fn deep_nesting_middle_level_continuation_bullets() {
1401 let content = "- L1\n - L2\n - L3\n\n continuation of L2\n";
1405 assert!(check(content).is_empty());
1406 }
1407
1408 #[test]
1409 fn deep_nesting_middle_level_continuation_ordered() {
1410 let content = "1. Level 1 item.\n1. Level 1 item:\n 1. Level 2 item.\n 1. Level 2 item.\n 1. Level 2 item:\n - Level 3 item.\n - Level 3 item.\n\n Level 2 list continuation.\n1. Level 1 item.\n";
1413 assert!(check(content).is_empty());
1414 }
1415
1416 #[test]
1417 fn deep_nesting_outermost_continuation() {
1418 let content = "- L1\n - L2\n - L3\n\n continuation of L1\n";
1421 assert!(check(content).is_empty());
1422 }
1423
1424 #[test]
1425 fn deep_nesting_between_levels_still_flagged() {
1426 let content = "- L1\n - L2\n - L3\n\n continuation\n";
1429 let warnings = check(content);
1430 assert_eq!(warnings.len(), 1);
1431 assert!(warnings[0].message.contains("4 spaces"));
1432 assert!(warnings[0].message.contains("found 3"));
1433 }
1434
1435 #[test]
1436 fn deep_nesting_beyond_deepest_still_flagged() {
1437 let content = "- L1\n - L2\n - L3\n\n continuation\n";
1439 let warnings = check(content);
1440 assert_eq!(warnings.len(), 1);
1441 assert!(warnings[0].message.contains("over-indented"));
1442 assert!(warnings[0].message.contains("expected 6, found 7"));
1443 }
1444
1445 #[test]
1446 fn four_levels_middle_continuation() {
1447 let content = "- L1\n - L2\n - L3\n - L4\n\n continuation of L2\n";
1450 assert!(check(content).is_empty());
1451 }
1452
1453 #[test]
1454 fn nested_sibling_closes_deeper_level() {
1455 let content = "- L1\n - L2a\n - L3\n - L2b\n\n continuation of L2b\n";
1458 assert!(check(content).is_empty());
1459 }
1460
1461 #[test]
1462 fn deep_nesting_middle_level_continuation_fix_preserved() {
1463 let content = "- L1\n - L2\n - L3\n\n continuation of L2\n";
1465 assert_eq!(fix(content), content);
1466 }
1467
1468 #[test]
1471 fn loose_tab_continuation_over_indented() {
1472 let content = "- Item\n\n\tcontinuation\n";
1477 let warnings = check(content);
1478 assert_eq!(warnings.len(), 1);
1479 assert_eq!(warnings[0].line, 3);
1480 assert_eq!(fix(content), "- Item\n\n continuation\n");
1481 }
1482
1483 #[test]
1486 fn multiple_continuations_correct() {
1487 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
1488 assert!(check(content).is_empty());
1489 }
1490
1491 #[test]
1492 fn multiple_continuations_second_under_indent() {
1493 let content = "- Item\n\n para 1\n\n continuation 2\n";
1495 let warnings = check(content);
1496 assert_eq!(warnings.len(), 1);
1497 assert_eq!(warnings[0].line, 5);
1498 }
1499
1500 #[test]
1503 fn ordered_paren_marker_correct() {
1504 let content = "1) Item\n\n continuation\n";
1506 assert!(check(content).is_empty());
1507 }
1508
1509 #[test]
1510 fn ordered_paren_marker_under_indent() {
1511 let content = "1) Item\n\n continuation\n";
1512 let warnings = check(content);
1513 assert_eq!(warnings.len(), 1);
1514 assert!(warnings[0].message.contains("3 spaces"));
1515 }
1516
1517 #[test]
1520 fn star_marker_correct() {
1521 let content = "* Item\n\n continuation\n";
1522 assert!(check(content).is_empty());
1523 }
1524
1525 #[test]
1526 fn star_marker_under_indent() {
1527 let content = "* Item\n\n continuation\n";
1528 let warnings = check(content);
1529 assert_eq!(warnings.len(), 1);
1530 }
1531
1532 #[test]
1533 fn plus_marker_correct() {
1534 let content = "+ Item\n\n continuation\n";
1535 assert!(check(content).is_empty());
1536 }
1537
1538 #[test]
1541 fn heading_after_list_no_warning() {
1542 let content = "- Item\n\n# Heading\n";
1543 assert!(check(content).is_empty());
1544 }
1545
1546 #[test]
1549 fn hr_after_list_no_warning() {
1550 let content = "- Item\n\n---\n";
1551 assert!(check(content).is_empty());
1552 }
1553
1554 #[test]
1557 fn reference_link_def_not_flagged() {
1558 let content = "- Item\n\n [link]: https://example.com\n";
1559 assert!(check(content).is_empty());
1560 }
1561
1562 #[test]
1565 fn footnote_def_not_flagged() {
1566 let content = "- Item\n\n [^1]: footnote text\n";
1567 assert!(check(content).is_empty());
1568 }
1569
1570 #[test]
1571 fn footnote_multiline_body_after_list_not_flagged() {
1572 let content = "# A list followed by a footnote\n\n\
1576 Here is a paragraph.[^fn]\n\n\
1577 - This is a list.\n\n\
1578 [^fn]:\n\
1579 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1580 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1581 assert!(check(content).is_empty());
1582 }
1583
1584 #[test]
1585 fn fix_footnote_multiline_body_after_list_is_noop() {
1586 let content = "# A list followed by a footnote\n\n\
1590 Here is a paragraph.[^fn]\n\n\
1591 - This is a list.\n\n\
1592 [^fn]:\n\
1593 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1594 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1595 assert_eq!(fix(content), content);
1596 }
1597
1598 #[test]
1599 fn footnote_body_indented_past_list_content_col_not_flagged() {
1600 let content = "- Item\n\n[^fn]:\n Body line one.\n Body line two.\n";
1604 assert!(check(content).is_empty());
1605 }
1606
1607 #[test]
1608 fn list_inside_footnote_body_continuation_not_flagged() {
1609 let content = "Text.[^fn]\n\n[^fn]:\n\
1613 \x20\x20\x20\x20- nested item\n\
1614 \x20\x20\x20\x20\x20\x20\x20over-indented continuation\n";
1615 assert!(check(content).is_empty());
1616 }
1617
1618 #[test]
1619 fn footnote_multiline_body_after_list_not_flagged_mkdocs() {
1620 let content = "Here is a paragraph.[^fn]\n\n\
1624 - This is a list.\n\n\
1625 [^fn]:\n\
1626 \x20\x20\x20\x20\x20\x20Footnote body that spans\n\
1627 \x20\x20\x20\x20\x20\x20multiple indented lines.\n";
1628 assert!(check_mkdocs(content).is_empty());
1629 }
1630
1631 #[test]
1634 fn fix_deep_nesting() {
1635 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
1636 let fixed = fix(content);
1637 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
1638 }
1639
1640 #[test]
1641 fn fix_mkdocs_unordered() {
1642 let content = "- Item\n\n continuation\n";
1644 let fixed = fix_mkdocs(content);
1645 assert_eq!(fixed, "- Item\n\n continuation\n");
1646 }
1647
1648 #[test]
1649 fn fix_code_fence_indent() {
1650 let content = "- Item\n\n ```\n code\n ```\n";
1653 let fixed = fix(content);
1654 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
1655 }
1656
1657 #[test]
1658 fn fix_mkdocs_code_fence_indent() {
1659 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
1661 let fixed = fix_mkdocs(content);
1662 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
1663 }
1664
1665 #[test]
1668 fn empty_document_no_warning() {
1669 assert!(check("").is_empty());
1670 }
1671
1672 #[test]
1673 fn whitespace_only_no_warning() {
1674 assert!(check(" \n\n \n").is_empty());
1675 }
1676
1677 #[test]
1680 fn no_list_no_warning() {
1681 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
1682 assert!(check(content).is_empty());
1683 }
1684
1685 #[test]
1688 fn multiline_continuation_all_lines_flagged() {
1689 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";
1690 let warnings = check(content);
1691 assert_eq!(warnings.len(), 3);
1692 assert_eq!(warnings[0].line, 3);
1693 assert_eq!(warnings[1].line, 4);
1694 assert_eq!(warnings[2].line, 5);
1695 }
1696
1697 #[test]
1698 fn multiline_continuation_with_frontmatter_fix() {
1699 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";
1700 let fixed = fix(content);
1701 assert_eq!(
1702 fixed,
1703 "---\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"
1704 );
1705 }
1706
1707 #[test]
1708 fn multiline_continuation_correct_indent_no_warning() {
1709 let content = "1. Item\n\n line one\n line two\n line three\n";
1710 assert!(check(content).is_empty());
1711 }
1712
1713 #[test]
1714 fn multiline_continuation_mixed_indent() {
1715 let content = "1. Item\n\n correct\n wrong\n correct\n";
1716 let warnings = check(content);
1717 assert_eq!(warnings.len(), 1);
1718 assert_eq!(warnings[0].line, 4);
1719 }
1720
1721 #[test]
1722 fn multiline_continuation_unordered() {
1723 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
1724 let warnings = check(content);
1725 assert_eq!(warnings.len(), 3);
1726 let fixed = fix(content);
1727 assert_eq!(
1728 fixed,
1729 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
1730 );
1731 }
1732
1733 #[test]
1734 fn multiline_continuation_two_items_fix() {
1735 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
1736 let fixed = fix(content);
1737 assert_eq!(
1738 fixed,
1739 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1740 );
1741 }
1742
1743 #[test]
1744 fn fence_fix_does_not_break_pairing_for_md031() {
1745 let content = "#### title\n\nabc\n\n\
1752 1. ab\n\n\
1753 \x20\x20`aabbccdd`\n\n\
1754 2. cd\n\n\
1755 \x20\x20`bbcc dd ee`\n\n\
1756 \x20\x20```\n\
1757 \x20\x20abcd\n\
1758 \x20\x20ef gh\n\
1759 \x20\x20```\n\n\
1760 \x20\x20uu\n\n\
1761 \x20\x20```\n\
1762 \x20\x20cdef\n\
1763 \x20\x20gh ij\n\
1764 \x20\x20```\n";
1765 let expected = "#### title\n\nabc\n\n\
1766 1. ab\n\n\
1767 \x20\x20\x20`aabbccdd`\n\n\
1768 2. cd\n\n\
1769 \x20\x20\x20`bbcc dd ee`\n\n\
1770 \x20\x20\x20```\n\
1771 \x20\x20\x20abcd\n\
1772 \x20\x20\x20ef gh\n\
1773 \x20\x20\x20```\n\n\
1774 \x20\x20\x20uu\n\n\
1775 \x20\x20\x20```\n\
1776 \x20\x20\x20cdef\n\
1777 \x20\x20\x20gh ij\n\
1778 \x20\x20\x20```\n";
1779 assert_eq!(fix(content), expected);
1780 }
1781
1782 #[test]
1783 fn multiline_continuation_separated_by_blank() {
1784 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1785 let warnings = check(content);
1786 assert_eq!(warnings.len(), 4);
1787 let fixed = fix(content);
1788 assert_eq!(
1789 fixed,
1790 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1791 );
1792 }
1793
1794 #[test]
1795 fn tab_indented_fence_is_normalized_to_spaces() {
1796 let content = "100. ab\n\n\t```\n\tabcd\n\t```\n";
1804 let expected = "100. ab\n\n ```\n abcd\n ```\n";
1805 assert_eq!(fix(content), expected);
1806 }
1807
1808 #[test]
1817 fn loose_continuation_over_indented_flagged() {
1818 let content = "* Item\n\n over-indented\n";
1821 let warnings = check(content);
1822 assert_eq!(warnings.len(), 1);
1823 assert_eq!(warnings[0].line, 3);
1824 assert!(warnings[0].message.contains("over-indented"));
1825 assert!(warnings[0].message.contains("expected 2"));
1826 assert!(warnings[0].message.contains("found 3"));
1827 }
1828
1829 #[test]
1830 fn loose_continuation_over_indented_multiline_mixed() {
1831 let content = "* Item\n\n over one\n correct\n over two\n";
1833 let warnings = check(content);
1834 assert_eq!(warnings.len(), 2);
1835 assert_eq!(warnings[0].line, 3);
1836 assert_eq!(warnings[1].line, 5);
1837 }
1838
1839 #[test]
1840 fn fix_loose_continuation_over_indented() {
1841 let content = "* Item\n\n over one\n correct\n over two\n";
1842 let fixed = fix(content);
1843 assert_eq!(fixed, "* Item\n\n over one\n correct\n over two\n");
1844 }
1845
1846 #[test]
1847 fn fix_tight_and_loose_items_normalized_identically() {
1848 let content = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1851 * 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\
1852 * 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";
1853 let expected = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1854 * 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\
1855 * 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";
1856 assert_eq!(fix(content), expected);
1857 }
1858
1859 #[test]
1860 fn multi_paragraph_item_loose_paragraph_over_indented() {
1861 let content = "* Item.\n tight over\n\n loose over\n";
1864 let warnings = check(content);
1865 assert_eq!(warnings.len(), 2);
1866 assert_eq!(warnings[0].line, 2);
1867 assert_eq!(warnings[1].line, 4);
1868 }
1869
1870 #[test]
1871 fn loose_indented_code_block_not_flagged() {
1872 let content = "- Item\n\n code line\n";
1876 assert!(check(content).is_empty());
1877 }
1878
1879 #[test]
1880 fn mkdocs_loose_over_indented_flagged() {
1881 let content = "1. Item\n\n over\n";
1884 let warnings = check_mkdocs(content);
1885 assert_eq!(warnings.len(), 1);
1886 assert_eq!(warnings[0].line, 3);
1887 assert!(warnings[0].message.contains("over-indented"));
1888 assert!(warnings[0].message.contains("expected 4"));
1889 assert!(warnings[0].message.contains("found 5"));
1890 }
1891
1892 #[test]
1893 fn task_list_loose_over_indented_flagged() {
1894 let content = "- [ ] Task\n\n over\n";
1897 let warnings = check(content);
1898 assert_eq!(warnings.len(), 1);
1899 assert_eq!(warnings[0].line, 3);
1900 }
1901
1902 #[test]
1903 fn loose_over_indent_boundary_below_code_block_threshold_flagged() {
1904 let content = "- Item\n\n over\n";
1909 let warnings = check(content);
1910 assert_eq!(warnings.len(), 1);
1911 assert_eq!(warnings[0].line, 3);
1912 assert!(warnings[0].message.contains("expected 2"));
1913 assert!(warnings[0].message.contains("found 5"));
1914 }
1915
1916 #[test]
1917 fn loose_over_indent_does_not_steal_nested_under_indent() {
1918 let content = "- Outer\n - Inner\n\n continuation\n";
1925 let warnings = check(content);
1926 assert_eq!(warnings.len(), 1);
1927 assert_eq!(warnings[0].line, 4);
1928 assert!(warnings[0].message.contains("4 spaces"));
1929 assert!(warnings[0].message.contains("found 3"));
1930 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1931 }
1932
1933 #[test]
1934 fn loose_over_indent_attributes_to_deepest_enclosing_item() {
1935 let content = "- Outer\n - Inner\n\n continuation\n";
1939 let warnings = check(content);
1940 assert_eq!(warnings.len(), 1);
1941 assert_eq!(warnings[0].line, 4);
1942 assert!(warnings[0].message.contains("expected 4"));
1943 assert!(warnings[0].message.contains("found 5"));
1944 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1945 }
1946
1947 #[test]
1956 fn loose_over_indented_fence_not_flagged() {
1957 let content = "- Item\n\n ```\n code\n ```\n";
1958 assert!(check(content).is_empty());
1959 assert_eq!(fix(content), content);
1960 }
1961
1962 #[test]
1963 fn tight_over_indented_fence_not_flagged() {
1964 let content = "- Item\n ```\n code\n ```\n";
1965 assert!(check(content).is_empty());
1966 assert_eq!(fix(content), content);
1967 }
1968
1969 #[test]
1970 fn over_indented_tilde_fence_not_flagged() {
1971 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1972 assert!(check(content).is_empty());
1973 assert_eq!(fix(content), content);
1974 }
1975
1976 #[test]
1977 fn fence_like_code_content_inside_fenced_block_not_flagged() {
1978 let content = "- Item\n\n ~~~\n ```\n ~~~\n";
1981 assert!(check(content).is_empty());
1982 assert_eq!(fix(content), content);
1983 }
1984
1985 #[test]
1986 fn unterminated_over_indented_fence_not_flagged() {
1987 let content = "- Item\n\n ```\n code1\n code2deeper\n";
1990 assert!(check(content).is_empty());
1991 assert_eq!(fix(content), content);
1992 }
1993
1994 #[test]
2002 fn task_list_tight_continuation_post_checkbox_reproducer_579() {
2003 let content = "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n";
2006 assert!(check(content).is_empty());
2007 }
2008
2009 #[test]
2010 fn task_list_tight_continuation_dash_unchecked() {
2011 let content = "- [ ] Task\n continuation\n";
2012 assert!(check(content).is_empty());
2013 }
2014
2015 #[test]
2016 fn task_list_tight_continuation_dash_checked_lower() {
2017 let content = "- [x] Task\n continuation\n";
2018 assert!(check(content).is_empty());
2019 }
2020
2021 #[test]
2022 fn task_list_tight_continuation_dash_checked_upper() {
2023 let content = "- [X] Task\n continuation\n";
2024 assert!(check(content).is_empty());
2025 }
2026
2027 #[test]
2028 fn task_list_tight_continuation_star_marker() {
2029 let content = "* [ ] Task\n continuation\n";
2030 assert!(check(content).is_empty());
2031 }
2032
2033 #[test]
2034 fn task_list_tight_continuation_plus_marker() {
2035 let content = "+ [ ] Task\n continuation\n";
2036 assert!(check(content).is_empty());
2037 }
2038
2039 #[test]
2040 fn task_list_tight_continuation_content_column_still_valid() {
2041 let content = "- [ ] Task\n continuation\n";
2044 assert!(check(content).is_empty());
2045 }
2046
2047 #[test]
2048 fn task_list_tight_continuation_between_columns_still_flagged() {
2049 let content = "- [ ] Task\n continuation\n";
2052 let warnings = check(content);
2053 assert_eq!(warnings.len(), 1);
2054 assert!(warnings[0].message.contains("expected 2 or 6"));
2056 assert!(warnings[0].message.contains("found 4"));
2057 }
2058
2059 #[test]
2060 fn task_list_tight_continuation_overshoot_still_flagged() {
2061 let content = "- [ ] Task\n continuation\n";
2063 let warnings = check(content);
2064 assert_eq!(warnings.len(), 1);
2065 assert!(warnings[0].message.contains("expected 2 or 6"));
2066 assert!(warnings[0].message.contains("found 7"));
2067 }
2068
2069 #[test]
2072 fn fix_task_list_overshoot_snaps_to_task_col() {
2073 let content = "- [ ] Task\n continuation\n";
2077 let fixed = fix(content);
2078 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2079 }
2080
2081 #[test]
2082 fn fix_task_list_col_5_snaps_to_task_col() {
2083 let content = "- [ ] Task\n continuation\n";
2085 let fixed = fix(content);
2086 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2087 }
2088
2089 #[test]
2090 fn fix_task_list_col_3_snaps_to_content_col() {
2091 let content = "- [ ] Task\n continuation\n";
2093 let fixed = fix(content);
2094 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2095 }
2096
2097 #[test]
2098 fn fix_task_list_col_4_ties_to_content_col() {
2099 let content = "- [ ] Task\n continuation\n";
2104 let fixed = fix(content);
2105 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2106 }
2107
2108 #[test]
2109 fn fix_task_list_ordered_overshoot_snaps_to_task_col() {
2110 let content = "1. [ ] Task\n continuation\n";
2113 let fixed = fix(content);
2114 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2115 }
2116
2117 #[test]
2118 fn fix_task_list_ordered_under_overshoot_snaps_to_content_col() {
2119 let content = "1. [ ] Task\n continuation\n";
2122 let fixed = fix(content);
2123 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2124 }
2125
2126 #[test]
2127 fn task_list_tight_continuation_ordered_single_digit() {
2128 let content = "1. [ ] Task\n continuation\n";
2130 assert!(check(content).is_empty());
2131 }
2132
2133 #[test]
2134 fn task_list_tight_continuation_ordered_multi_digit() {
2135 let content = "10. [ ] Task\n continuation\n";
2137 assert!(check(content).is_empty());
2138 }
2139
2140 #[test]
2141 fn task_list_tight_continuation_nested_dash() {
2142 let content = "- Parent\n - [ ] Nested task\n continuation\n";
2144 assert!(check(content).is_empty());
2145 }
2146
2147 #[test]
2148 fn task_list_loose_continuation_post_checkbox_column_not_flagged() {
2149 let content = "- [ ] Task\n\n continuation\n";
2154 assert!(check(content).is_empty());
2155 }
2156
2157 #[test]
2158 fn task_list_empty_body_is_not_a_task() {
2159 let content = "- [ ]\n continuation\n";
2165 let warnings = check(content);
2166 assert_eq!(warnings.len(), 1);
2167 assert!(warnings[0].message.contains("found 4"));
2168 }
2169
2170 #[test]
2171 fn task_list_malformed_checkbox_is_not_a_task() {
2172 let content = "- [~] Not a task\n continuation\n";
2174 let warnings = check(content);
2175 assert_eq!(warnings.len(), 1);
2176 }
2177
2178 #[test]
2185 fn task_list_mkdocs_unordered_required_min_valid() {
2186 let content = "- [ ] Task\n continuation\n";
2188 assert!(check_mkdocs(content).is_empty());
2189 }
2190
2191 #[test]
2192 fn task_list_mkdocs_unordered_post_checkbox_valid() {
2193 let content = "- [ ] Task\n continuation\n";
2194 assert!(check_mkdocs(content).is_empty());
2195 }
2196
2197 #[test]
2198 fn task_list_mkdocs_unordered_between_flagged() {
2199 let content = "- [ ] Task\n continuation\n";
2201 let warnings = check_mkdocs(content);
2202 assert_eq!(warnings.len(), 1);
2203 }
2204
2205 #[test]
2206 fn task_list_mkdocs_ordered_both_columns_valid() {
2207 let at_4 = "1. [ ] Task\n continuation\n";
2209 assert!(check_mkdocs(at_4).is_empty());
2210 let at_7 = "1. [ ] Task\n continuation\n";
2211 assert!(check_mkdocs(at_7).is_empty());
2212 }
2213
2214 #[test]
2215 fn task_list_mkdocs_ordered_between_flagged() {
2216 let at_5 = "1. [ ] Task\n continuation\n";
2218 assert_eq!(check_mkdocs(at_5).len(), 1);
2219 let at_6 = "1. [ ] Task\n continuation\n";
2220 assert_eq!(check_mkdocs(at_6).len(), 1);
2221 }
2222
2223 #[test]
2233 fn fix_task_list_tie_sibling_at_task_col_snaps_to_task_col() {
2234 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
2238 let fixed = fix(content);
2239 assert_eq!(
2240 fixed,
2241 "- [ ] Task\n aligned continuation\n tied continuation\n"
2242 );
2243 }
2244
2245 #[test]
2246 fn fix_task_list_tie_sibling_at_content_col_snaps_to_content_col() {
2247 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
2250 let fixed = fix(content);
2251 assert_eq!(fixed, "- [ ] Task\n aligned continuation\n tied continuation\n");
2252 }
2253
2254 #[test]
2255 fn fix_task_list_tie_both_siblings_snaps_to_content_col() {
2256 let content = "- [ ] Task\n at content col\n at task col\n tied continuation\n";
2260 let fixed = fix(content);
2261 assert_eq!(
2262 fixed,
2263 "- [ ] Task\n at content col\n at task col\n tied continuation\n"
2264 );
2265 }
2266
2267 #[test]
2268 fn fix_task_list_tie_sees_task_col_through_tight_lazy_continuation() {
2269 let content = concat!("- [ ] Task\n", "lazy\n", " aligned at task col\n", " tied\n",);
2283 let fixed = fix(content);
2284 assert!(
2285 fixed.contains("\n tied\n"),
2286 "tied line should snap to col 6 (task col) because a task-col \
2287 sibling is visible past the tight lazy-continuation line; got:\n{fixed}"
2288 );
2289 }
2290
2291 #[test]
2298 fn task_list_tab_indented_continuation_flagged() {
2299 let content = "- [ ] Task\n\t\twrap\n";
2302 let warnings = check(content);
2303 assert_eq!(warnings.len(), 1);
2304 assert!(warnings[0].message.contains("expected 2 or 6"));
2305 assert!(warnings[0].message.contains("found 8"));
2306 }
2307
2308 #[test]
2309 fn fix_task_list_tab_indented_snaps_to_task_col() {
2310 let content = "- [ ] Task\n\t\twrap\n";
2312 let fixed = fix(content);
2313 assert_eq!(fixed, "- [ ] Task\n wrap\n");
2314 }
2315
2316 #[test]
2317 fn fix_task_list_single_tab_equidistant_snaps_to_content_col() {
2318 let content = "- [ ] Task\n\twrap\n";
2321 let fixed = fix(content);
2322 assert_eq!(fixed, "- [ ] Task\n wrap\n");
2323 }
2324
2325 #[test]
2335 fn task_list_blockquote_post_checkbox_not_flagged() {
2336 let content = "> - [ ] Task\n> continuation\n";
2338 assert!(check(content).is_empty());
2339 }
2340
2341 #[test]
2342 fn task_list_blockquote_between_cols_documented_limitation() {
2343 let content = "> - [ ] Task\n> continuation\n";
2347 assert!(check(content).is_empty());
2348 }
2349
2350 #[test]
2351 fn task_list_blockquote_overshoot_documented_limitation() {
2352 let content = "> - [ ] Task\n> continuation\n";
2354 assert!(check(content).is_empty());
2355 }
2356
2357 #[test]
2364 fn fix_task_list_mkdocs_unordered_overshoot_snaps_to_task_col() {
2365 let content = "- [ ] Task\n continuation\n";
2368 let fixed = fix_mkdocs(content);
2369 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2370 }
2371
2372 #[test]
2373 fn fix_task_list_mkdocs_unordered_tie_snaps_to_required() {
2374 let content = "- [ ] Task\n continuation\n";
2377 let fixed = fix_mkdocs(content);
2378 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2379 }
2380
2381 #[test]
2382 fn fix_task_list_mkdocs_ordered_overshoot_snaps_to_task_col() {
2383 let content = "1. [ ] Task\n continuation\n";
2386 let fixed = fix_mkdocs(content);
2387 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2388 }
2389
2390 #[test]
2391 fn fix_task_list_mkdocs_ordered_near_required_snaps_to_required() {
2392 let content = "1. [ ] Task\n continuation\n";
2398 let fixed = fix_mkdocs(content);
2399 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2400 }
2401
2402 #[test]
2403 fn fix_task_list_mkdocs_ordered_between_cols_snaps_to_task_col() {
2404 let content = "1. [ ] Task\n continuation\n";
2407 let fixed = fix_mkdocs(content);
2408 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2409 }
2410
2411 fn assert_idempotent(content: &str) {
2421 let once = fix(content);
2422 let twice = fix(&once);
2423 assert_eq!(once, twice, "MD077 fix was not idempotent on input: {content:?}");
2424 }
2425
2426 fn assert_idempotent_mkdocs(content: &str) {
2427 let once = fix_mkdocs(content);
2428 let twice = fix_mkdocs(&once);
2429 assert_eq!(
2430 once, twice,
2431 "MD077 (MkDocs) fix was not idempotent on input: {content:?}"
2432 );
2433 }
2434
2435 #[test]
2436 fn idempotent_task_list_between_cols() {
2437 assert_idempotent("- [ ] Task\n continuation\n");
2438 }
2439
2440 #[test]
2441 fn idempotent_task_list_overshoot() {
2442 assert_idempotent("- [ ] Task\n continuation\n");
2443 }
2444
2445 #[test]
2446 fn idempotent_task_list_under_post_checkbox() {
2447 assert_idempotent("- [ ] Task\n continuation\n");
2448 }
2449
2450 #[test]
2451 fn idempotent_task_list_near_post_checkbox() {
2452 assert_idempotent("- [ ] Task\n continuation\n");
2453 }
2454
2455 #[test]
2456 fn idempotent_task_list_tab_overshoot() {
2457 assert_idempotent("- [ ] Task\n\t\twrap\n");
2458 }
2459
2460 #[test]
2461 fn idempotent_task_list_single_tab() {
2462 assert_idempotent("- [ ] Task\n\twrap\n");
2463 }
2464
2465 #[test]
2466 fn idempotent_task_list_ordered_overshoot() {
2467 assert_idempotent("1. [ ] Task\n continuation\n");
2468 }
2469
2470 #[test]
2471 fn idempotent_task_list_ordered_under() {
2472 assert_idempotent("1. [ ] Task\n continuation\n");
2473 }
2474
2475 #[test]
2476 fn idempotent_task_list_tie_with_sibling_at_task_col() {
2477 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2478 }
2479
2480 #[test]
2481 fn idempotent_task_list_tie_with_sibling_at_content_col() {
2482 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2483 }
2484
2485 #[test]
2486 fn idempotent_task_list_mkdocs_unordered_overshoot() {
2487 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2488 }
2489
2490 #[test]
2491 fn idempotent_task_list_mkdocs_unordered_tie() {
2492 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2493 }
2494
2495 #[test]
2496 fn idempotent_task_list_mkdocs_ordered_overshoot() {
2497 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2498 }
2499
2500 #[test]
2501 fn idempotent_task_list_mkdocs_ordered_between() {
2502 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2503 }
2504
2505 #[test]
2506 fn idempotent_task_list_reproducer_579() {
2507 assert_idempotent(
2511 "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n",
2512 );
2513 }
2514
2515 #[test]
2516 fn idempotent_non_task_list_still_holds() {
2517 assert_idempotent("1. Item\n over-indented\n");
2520 assert_idempotent("- Item\n\n continuation\n");
2521 }
2522
2523 #[test]
2530 fn idempotent_non_task_loose_under_indent_ordered() {
2531 assert_idempotent("1. Item\n\n continuation\n");
2533 }
2534
2535 #[test]
2536 fn idempotent_non_task_loose_under_indent_multi_digit() {
2537 assert_idempotent("10. Item\n\n continuation\n");
2539 }
2540
2541 #[test]
2542 fn idempotent_non_task_tight_over_indent_ordered() {
2543 assert_idempotent("1. Item\n over-indented\n");
2545 }
2546
2547 #[test]
2555 fn idempotent_non_task_fence_ordered_loose() {
2556 assert_idempotent("1. Item\n\n ```rust\n let x = 1;\n ```\n");
2558 }
2559
2560 #[test]
2561 fn idempotent_non_task_fence_tilde_under_indent() {
2562 assert_idempotent("1. Item\n\n ~~~\nplain text\n ~~~\n");
2568 }
2569
2570 #[test]
2571 fn idempotent_non_task_fence_interior_above_required() {
2572 assert_idempotent("1. Item\n\n ```\n deeply indented code\n ```\n");
2576 }
2577
2578 #[test]
2579 fn fence_fix_promotes_interior_below_scope_in_single_pass() {
2580 let content = "1. Item\n\n ```\ncode\n ```\n";
2584 let fixed = fix(content);
2585 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2586 }
2587
2588 #[test]
2589 fn fence_fix_preserves_interior_above_required() {
2590 let content = "1. Item\n\n ```\n code\n ```\n";
2593 let fixed = fix(content);
2594 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2595 }
2596
2597 #[test]
2604 fn idempotent_non_task_mkdocs_ordered_at_3_spaces() {
2605 assert_idempotent_mkdocs("1. Item\n\n continuation\n");
2607 }
2608
2609 #[test]
2610 fn idempotent_non_task_mkdocs_unordered_at_2_spaces() {
2611 assert_idempotent_mkdocs("- Item\n\n continuation\n");
2613 }
2614
2615 #[test]
2616 fn idempotent_non_task_mkdocs_fence_compound() {
2617 assert_idempotent_mkdocs("1. Item\n\n ```toml\n k = 1\n ```\n");
2619 }
2620
2621 #[test]
2624 fn aligned_tight_zero_indent_continuation_flagged() {
2625 let content = "- this is a long line\nthat continues on a second line\n";
2629 let warnings = check_aligned(content);
2630 assert_eq!(warnings.len(), 1);
2631 assert_eq!(warnings[0].line, 2);
2632 assert_eq!(
2633 fix_aligned(content),
2634 "- this is a long line\n that continues on a second line\n"
2635 );
2636 }
2637
2638 #[test]
2639 fn aligned_full_issue_example_made_consistent() {
2640 let content = "- this is a long line\n\
2643 that continues on a second line\n\
2644 - this is another long line\n\
2645 \x20\x20that continues on the next line\n\
2646 - yet again a long line\n\
2647 and still inconsistently spaced\n\
2648 \x20\x20and even worse\n";
2649 let expected = "- this is a long line\n\
2650 \x20\x20that continues on a second line\n\
2651 - this is another long line\n\
2652 \x20\x20that continues on the next line\n\
2653 - yet again a long line\n\
2654 \x20\x20and still inconsistently spaced\n\
2655 \x20\x20and even worse\n";
2656 assert_eq!(fix_aligned(content), expected);
2657 assert_eq!(fix_aligned(expected), expected);
2659 }
2660
2661 #[test]
2662 fn aligned_already_aligned_not_flagged() {
2663 let content = "- item\n continuation at content column\n";
2664 assert!(check_aligned(content).is_empty());
2665 }
2666
2667 #[test]
2668 fn aligned_tight_partial_indent_flagged() {
2669 let content = "- item\n continuation\n";
2671 let warnings = check_aligned(content);
2672 assert_eq!(warnings.len(), 1);
2673 assert_eq!(fix_aligned(content), "- item\n continuation\n");
2674 }
2675
2676 #[test]
2677 fn aligned_post_blank_zero_indent_still_new_paragraph() {
2678 let content = "- item\n\nnew paragraph\n";
2681 assert!(check_aligned(content).is_empty());
2682 assert_eq!(fix_aligned(content), content);
2683 }
2684
2685 #[test]
2688 fn aligned_top_level_blockquote_after_list_untouched() {
2689 let content = "- item\n> quote\n";
2693 assert!(check_aligned(content).is_empty());
2694 assert_eq!(fix_aligned(content), content);
2695 }
2696
2697 #[test]
2698 fn aligned_top_level_fence_after_list_untouched() {
2699 let content = "- item\n```\ncode\n```\n";
2700 assert!(check_aligned(content).is_empty());
2701 assert_eq!(fix_aligned(content), content);
2702 }
2703
2704 #[test]
2705 fn aligned_top_level_table_after_list_untouched() {
2706 let content = "- item\n| a | b |\n|---|---|\n| 1 | 2 |\n";
2707 assert!(check_aligned(content).is_empty());
2708 assert_eq!(fix_aligned(content), content);
2709 }
2710
2711 #[test]
2714 fn aligned_nested_tight_lazy_continuation_aligns_to_inner() {
2715 let content = "- Outer\n - Inner\ncontinuation\n";
2720 let warnings = check_aligned(content);
2721 assert_eq!(warnings.len(), 1);
2722 assert_eq!(fix_aligned(content), "- Outer\n - Inner\n continuation\n");
2723 }
2724
2725 #[test]
2726 fn aligned_nested_continuation_already_aligned_not_flagged() {
2727 let content = "- L1\n - L2\n cont of L2 at 4\n";
2728 assert!(check_aligned(content).is_empty());
2729 }
2730
2731 #[test]
2732 fn aligned_nested_idempotent() {
2733 let content = "- Outer\n - Inner\ncontinuation\n";
2734 let once = fix_aligned(content);
2735 assert_eq!(fix_aligned(&once), once);
2736 }
2737
2738 #[test]
2739 fn aligned_three_level_nesting_aligns_to_innermost() {
2740 let content = "- L1\n - L2\n - L3\ncont\n";
2743 assert_eq!(fix_aligned(content), "- L1\n - L2\n - L3\n cont\n");
2744 }
2745
2746 #[test]
2747 fn aligned_continuation_after_sibling_owned_by_last_item() {
2748 let content = "- a\n- b\nlazy\n";
2751 assert_eq!(fix_aligned(content), "- a\n- b\n lazy\n");
2752 }
2753
2754 #[test]
2755 fn aligned_multi_digit_ordered_marker_aligns_to_content_column() {
2756 let content = "10. Item\nwrap\n";
2757 assert_eq!(fix_aligned(content), "10. Item\n wrap\n");
2758 }
2759
2760 #[test]
2761 fn aligned_setext_heading_after_list_left_alone() {
2762 let content = "- item\nText\n===\n";
2765 assert!(check_aligned(content).is_empty());
2766 assert_eq!(fix_aligned(content), content);
2767 }
2768
2769 #[test]
2770 fn aligned_latent_marker_in_continuation_is_idempotent() {
2771 let content = "# \n- \n``\n2. \n![]()";
2777 let once = fix_aligned(content);
2778 assert_eq!(fix_aligned(&once), once, "fix must be idempotent in one pass");
2779 assert_eq!(once, content, "item with a latent marker is left untouched");
2780 }
2781
2782 #[test]
2783 fn aligned_latent_table_in_continuation_is_idempotent() {
2784 let content = "- \n![`]()\n| | ` |\n| --- | --- |";
2789 let once = fix_aligned(content);
2790 assert_eq!(fix_aligned(&once), once, "fix must be idempotent in one pass");
2791 assert_eq!(once, content, "item with a latent table is left untouched");
2792 }
2793
2794 #[test]
2795 fn aligned_blockquote_nested_list_not_touched() {
2796 let content = "> - item\n> wrap\n";
2800 assert!(check_aligned(content).is_empty());
2801 assert_eq!(fix_aligned(content), content);
2802 }
2803
2804 #[test]
2807 fn aligned_task_post_checkbox_column_accepted() {
2808 let content = "- [ ] Task\n wrap\n";
2811 assert!(check_aligned(content).is_empty());
2812 assert_eq!(fix_aligned(content), content);
2813 }
2814
2815 #[test]
2816 fn aligned_task_under_indent_snaps_to_content_column() {
2817 let content = "- [ ] Task\nwrap\n";
2818 let warnings = check_aligned(content);
2819 assert_eq!(warnings.len(), 1);
2820 assert_eq!(fix_aligned(content), "- [ ] Task\n wrap\n");
2821 }
2822
2823 #[test]
2826 fn aligned_mkdocs_tight_under_indent_snaps_to_four() {
2827 let content = "- item\nwrap\n";
2829 let warnings = check_aligned_mkdocs(content);
2830 assert_eq!(warnings.len(), 1);
2831 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
2832 assert_eq!(aligned_rule().fix(&ctx).unwrap(), "- item\n wrap\n");
2833 }
2834
2835 #[test]
2838 fn any_default_does_not_flag_tight_lazy_continuation() {
2839 let content = "- item\nwrapped at zero indent\n";
2841 assert!(check(content).is_empty());
2842 assert_eq!(fix(content), content);
2843 }
2844
2845 #[test]
2846 fn from_config_aligned_enables_tight_flagging() {
2847 let mut config = crate::config::Config::default();
2849 let mut rule_config = crate::config::RuleConfig::default();
2850 rule_config
2851 .values
2852 .insert("style".to_string(), toml::Value::String("aligned".to_string()));
2853 config.rules.insert("MD077".to_string(), rule_config);
2854
2855 let rule = MD077ListContinuationIndent::from_config(&config);
2856 let ctx = LintContext::new("- item\nwrap\n", MarkdownFlavor::Standard, None);
2857 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
2858 }
2859
2860 #[test]
2861 fn from_config_default_is_any() {
2862 let config = crate::config::Config::default();
2864 let rule = MD077ListContinuationIndent::from_config(&config);
2865 let ctx = LintContext::new("- item\nwrap\n", MarkdownFlavor::Standard, None);
2866 assert!(rule.check(&ctx).unwrap().is_empty());
2867 }
2868
2869 #[test]
2870 fn aligned_tight_underindented_fence_inside_item_left_alone() {
2871 let content = "- item\n ```\n code\n ```\n";
2875 assert!(check_aligned(content).is_empty());
2876 assert_eq!(fix_aligned(content), content);
2877 }
2878
2879 #[test]
2880 fn aligned_task_under_indent_fix_is_idempotent() {
2881 let content = "- [ ] Task\nwrap\n";
2882 let once = fix_aligned(content);
2883 assert_eq!(fix_aligned(&once), once);
2884 }
2885
2886 #[test]
2887 fn aligned_partial_indent_fix_is_idempotent() {
2888 let content = "- item\n continuation\n";
2889 let once = fix_aligned(content);
2890 assert_eq!(fix_aligned(&once), once);
2891 }
2892}