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 if info.blockquote.is_some() {
609 continue;
610 }
611 let line = info.content(ctx.content);
612 let task_col = Self::is_task_list_item(line, li.content_column)
613 .then_some(li.content_column + Self::TASK_CHECKBOX_PREFIX_LEN);
614 items.push((item_line, li.marker_column, li.content_column, task_col));
615 }
616 }
617 }
618 items.sort_unstable();
619 items.dedup_by_key(|&mut (ln, _, _, _)| ln);
620
621 let mut range_ends = vec![total_lines; items.len()];
634 let mut stack: Vec<usize> = Vec::new();
635 for i in (0..items.len()).rev() {
636 let marker_col = items[i].1;
637 while let Some(&top) = stack.last() {
638 if items[top].1 > marker_col {
639 stack.pop();
640 } else {
641 break;
642 }
643 }
644 range_ends[i] = stack.last().map_or(total_lines, |&j| items[j].0 - 1);
645 stack.push(i);
646 }
647
648 let scoped: Vec<(usize, usize, usize, Option<usize>, usize, usize)> = items
651 .iter()
652 .enumerate()
653 .map(|(item_idx, &(item_line, marker_col, content_col, task_col))| {
654 let required = if strict_indent { content_col.max(4) } else { content_col };
655 (
656 item_line,
657 marker_col,
658 content_col,
659 task_col,
660 required,
661 range_ends[item_idx],
662 )
663 })
664 .collect();
665
666 let prose_candidate_lines: Vec<usize> = (1..=total_lines)
680 .filter(|&line_num| {
681 let Some(info) = ctx.line_info(line_num) else {
682 return false;
683 };
684 let trimmed = info.content(ctx.content).trim_start();
685 !Self::should_skip_line(info, trimmed)
686 && !info.is_blank
687 && info.list_item.is_none()
688 && info.heading.is_none()
689 && !info.is_horizontal_rule
690 && !Self::is_block_level_construct(trimmed)
691 })
692 .collect();
693 let range_has_prose_candidate = |after_line: usize, range_end: usize| -> bool {
696 let start = prose_candidate_lines.partition_point(|&l| l <= after_line);
697 prose_candidate_lines.get(start).is_some_and(|&l| l <= range_end)
698 };
699
700 let aligned = self.config.style == ContinuationStyle::Aligned;
726 for &(item_line, marker_col, _content_col, _task_col, required, range_end) in &scoped {
727 if !range_has_prose_candidate(item_line, range_end) {
730 continue;
731 }
732 let has_latent_structure = aligned && Self::item_range_has_latent_structure(ctx, item_line, range_end);
747 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
748 let actual = line.actual;
749 let under_indented = actual < required;
750 let loose_escape = line.saw_blank && under_indented;
751 let confirmed_structure = line.info.in_code_block || line.info.blockquote.is_some();
758 let aligned_tight = aligned
759 && !has_latent_structure
760 && !line.saw_blank
761 && !line.saw_nested
762 && under_indented
763 && !confirmed_structure;
764 if (loose_escape || aligned_tight) && flagged_lines.insert(line.line_num) {
765 let message = if line.saw_blank {
766 if strict_indent {
767 format!(
768 "Content inside list item needs {required} spaces of indentation \
769 for MkDocs compatibility (found {actual})",
770 )
771 } else {
772 format!(
773 "Content after blank line in list item needs {required} spaces of \
774 indentation to remain part of the list (found {actual})",
775 )
776 }
777 } else {
778 format!("Continuation line under-indented (expected {required}, found {actual})")
779 };
780 let outcome = Self::build_under_indent_warning(ctx, line, required, message);
781 if let Some(closer_line) = outcome.also_flag_line {
782 flagged_lines.insert(closer_line);
783 }
784 warnings.push(outcome.warning);
785 }
786 ControlFlow::Continue(())
787 });
788 }
789
790 for &(item_line, marker_col, content_col, task_col, required, range_end) in &scoped {
799 if !range_has_prose_candidate(item_line, range_end) {
801 continue;
802 }
803 let (uses_content_col, uses_task_col) = match task_col {
807 Some(t) => Self::sibling_column_usage(ctx, item_line, range_end, marker_col, content_col, t),
808 None => (false, false),
809 };
810
811 Self::walk_item_continuation(ctx, item_line, range_end, marker_col, |line| {
812 let actual = line.actual;
813 if actual > required
814 && !line.info.in_code_block
815 && Some(actual) != task_col
816 && !Self::starts_with_list_marker(line.trimmed)
817 && flagged_lines.insert(line.line_num)
818 {
819 let fix_target =
820 Self::compute_fix_target(actual, required, task_col, uses_content_col, uses_task_col);
821 let message = match task_col {
822 Some(t) => format!(
823 "Continuation line over-indented \
824 (expected {required} or {t}, found {actual})"
825 ),
826 None => {
827 format!("Continuation line over-indented (expected {required}, found {actual})")
828 }
829 };
830 warnings.push(Self::build_over_indent_warning(ctx, line, fix_target, message));
831 }
832 ControlFlow::Continue(())
833 });
834 }
835
836 warnings.sort_by_key(|w| (w.line, w.column));
839
840 Ok(warnings)
841 }
842
843 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
844 let warnings = self.check(ctx)?;
845 let warnings =
846 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
847 if warnings.is_empty() {
848 return Ok(ctx.content.to_string());
849 }
850
851 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
853 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
854
855 let mut content = ctx.content.to_string();
856 for fix in fixes {
857 if fix.range.start <= content.len() && fix.range.end <= content.len() {
858 content.replace_range(fix.range, &fix.replacement);
859 }
860 }
861
862 Ok(content)
863 }
864
865 fn category(&self) -> RuleCategory {
866 RuleCategory::List
867 }
868
869 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
870 ctx.content.is_empty() || ctx.list_blocks.is_empty()
871 }
872
873 fn as_any(&self) -> &dyn std::any::Any {
874 self
875 }
876
877 crate::impl_rule_config_methods!(MD077Config);
878}
879
880#[cfg(test)]
881mod tests {
882 use super::*;
883 use crate::config::MarkdownFlavor;
884
885 fn check(content: &str) -> Vec<LintWarning> {
886 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
887 let rule = MD077ListContinuationIndent::default();
888 rule.check(&ctx).unwrap()
889 }
890
891 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
892 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
893 let rule = MD077ListContinuationIndent::default();
894 rule.check(&ctx).unwrap()
895 }
896
897 fn fix(content: &str) -> String {
898 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
899 let rule = MD077ListContinuationIndent::default();
900 rule.fix(&ctx).unwrap()
901 }
902
903 fn fix_mkdocs(content: &str) -> String {
904 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
905 let rule = MD077ListContinuationIndent::default();
906 rule.fix(&ctx).unwrap()
907 }
908
909 fn aligned_rule() -> MD077ListContinuationIndent {
910 MD077ListContinuationIndent::new(ContinuationStyle::Aligned)
911 }
912
913 fn check_aligned(content: &str) -> Vec<LintWarning> {
914 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
915 aligned_rule().check(&ctx).unwrap()
916 }
917
918 fn check_aligned_mkdocs(content: &str) -> Vec<LintWarning> {
919 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
920 aligned_rule().check(&ctx).unwrap()
921 }
922
923 fn fix_aligned(content: &str) -> String {
924 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
925 aligned_rule().fix(&ctx).unwrap()
926 }
927
928 fn fix_aligned_quarto(content: &str) -> String {
929 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
930 aligned_rule().fix(&ctx).unwrap()
931 }
932
933 #[test]
934 fn aligned_idempotent_with_latent_marker_behind_unstable_heading() {
935 let input = "1. \n``\n``\n- \n``";
944 let once = fix_aligned_quarto(input);
945 let twice = fix_aligned_quarto(&once);
946 assert_eq!(once, twice, "MD077 aligned fix must be idempotent (Quarto)");
947 }
948
949 #[test]
950 fn aligned_idempotent_with_lazy_continuation_out_of_a_blockquote() {
951 let input = "- \n> *\n> a\n``";
955 let once = fix_aligned(input);
956 let twice = fix_aligned(&once);
957 assert_eq!(once, twice, "MD077 aligned fix must be idempotent");
958 }
959
960 #[test]
963 fn tight_lazy_continuation_zero_indent_not_flagged() {
964 let content = "- Item\ncontinuation\n";
966 assert!(check(content).is_empty());
967 }
968
969 #[test]
970 fn tight_continuation_correct_indent_not_flagged() {
971 let content = "1. Item\n continuation\n";
973 assert!(check(content).is_empty());
974 }
975
976 #[test]
977 fn tight_continuation_over_indented_ordered() {
978 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
980 let warnings = check(content);
981 assert_eq!(warnings.len(), 1);
982 assert_eq!(warnings[0].line, 2);
983 assert!(warnings[0].message.contains("over-indented"));
984 }
985
986 #[test]
987 fn tight_continuation_over_indented_unordered() {
988 let content = "- Item\n over-indented\n";
990 let warnings = check(content);
991 assert_eq!(warnings.len(), 1);
992 assert_eq!(warnings[0].line, 2);
993 }
994
995 #[test]
996 fn tight_continuation_multiple_over_indented_lines() {
997 let content = "1. Item\n line one\n line two\n line three\n";
998 let warnings = check(content);
999 assert_eq!(warnings.len(), 3);
1000 }
1001
1002 #[test]
1003 fn tight_continuation_mixed_correct_and_over() {
1004 let content = "1. Item\n correct\n over-indented\n correct again\n";
1005 let warnings = check(content);
1006 assert_eq!(warnings.len(), 1);
1007 assert_eq!(warnings[0].line, 3);
1008 }
1009
1010 #[test]
1011 fn tight_continuation_nested_over_indented() {
1012 let content = "- L1\n - L2\n over-indented continuation of L2\n";
1014 let warnings = check(content);
1015 assert_eq!(warnings.len(), 1);
1016 assert_eq!(warnings[0].line, 3);
1017 assert!(warnings[0].message.contains("expected 4"));
1019 assert!(warnings[0].message.contains("found 5"));
1020 }
1021
1022 #[test]
1023 fn tight_continuation_nested_correct_indent_not_flagged() {
1024 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
1027 assert!(check(content).is_empty());
1028 }
1029
1030 #[test]
1031 fn fix_tight_continuation_nested_over_indented() {
1032 let content = "- L1\n - L2\n over-indented continuation of L2\n";
1034 let fixed = fix(content);
1035 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
1036 }
1037
1038 #[test]
1039 fn tight_continuation_under_indented_not_flagged() {
1040 let content = "1. Item\n under-indented\n";
1043 assert!(check(content).is_empty());
1044 }
1045
1046 #[test]
1047 fn tight_continuation_tab_over_indented() {
1048 let content = "- Item\n\tover-indented\n";
1050 let warnings = check(content);
1051 assert_eq!(warnings.len(), 1);
1052 }
1053
1054 #[test]
1055 fn fix_tight_continuation_over_indented_ordered() {
1056 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
1057 let fixed = fix(content);
1058 assert_eq!(
1059 fixed,
1060 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
1061 );
1062 }
1063
1064 #[test]
1065 fn fix_tight_continuation_over_indented_unordered() {
1066 let content = "- Item\n over-indented\n";
1067 let fixed = fix(content);
1068 assert_eq!(fixed, "- Item\n over-indented\n");
1069 }
1070
1071 #[test]
1072 fn fix_tight_continuation_multiple_lines() {
1073 let content = "1. Item\n line one\n line two\n";
1074 let fixed = fix(content);
1075 assert_eq!(fixed, "1. Item\n line one\n line two\n");
1076 }
1077
1078 #[test]
1079 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
1080 let content = "1. Item\n continuation\n";
1083 assert!(check_mkdocs(content).is_empty());
1084 }
1085
1086 #[test]
1087 fn tight_continuation_mkdocs_5space_ordered_flagged() {
1088 let content = "1. Item\n over-indented\n";
1090 let warnings = check_mkdocs(content);
1091 assert_eq!(warnings.len(), 1);
1092 assert!(warnings[0].message.contains("expected 4"));
1093 assert!(warnings[0].message.contains("found 5"));
1094 }
1095
1096 #[test]
1097 fn fix_tight_continuation_mkdocs_over_indented() {
1098 let content = "1. Item\n over-indented\n";
1099 let fixed = fix_mkdocs(content);
1100 assert_eq!(fixed, "1. Item\n over-indented\n");
1101 }
1102
1103 #[test]
1104 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
1105 let content = "* Level 0\n * Level 1\n * Level 2\n";
1108 assert!(check(content).is_empty());
1109 }
1110
1111 #[test]
1112 fn tight_continuation_ordered_marker_not_flagged() {
1113 let content = "- Parent\n 1. Child item\n";
1115 assert!(check(content).is_empty());
1116 }
1117
1118 #[test]
1121 fn unordered_correct_indent_no_warning() {
1122 let content = "- Item\n\n continuation\n";
1123 assert!(check(content).is_empty());
1124 }
1125
1126 #[test]
1127 fn unordered_partial_indent_warns() {
1128 let content = "- Item\n\n continuation\n";
1131 let warnings = check(content);
1132 assert_eq!(warnings.len(), 1);
1133 assert_eq!(warnings[0].line, 3);
1134 assert!(warnings[0].message.contains("2 spaces"));
1135 assert!(warnings[0].message.contains("found 1"));
1136 }
1137
1138 #[test]
1139 fn unordered_zero_indent_is_new_paragraph() {
1140 let content = "- Item\n\ncontinuation\n";
1143 assert!(check(content).is_empty());
1144 }
1145
1146 #[test]
1149 fn ordered_3space_correct_commonmark() {
1150 let content = "1. Item\n\n continuation\n";
1152 assert!(check(content).is_empty());
1153 }
1154
1155 #[test]
1156 fn ordered_2space_under_indent_commonmark() {
1157 let content = "1. Item\n\n continuation\n";
1158 let warnings = check(content);
1159 assert_eq!(warnings.len(), 1);
1160 assert!(warnings[0].message.contains("3 spaces"));
1161 assert!(warnings[0].message.contains("found 2"));
1162 }
1163
1164 #[test]
1167 fn multi_digit_marker_correct() {
1168 let content = "10. Item\n\n continuation\n";
1170 assert!(check(content).is_empty());
1171 }
1172
1173 #[test]
1174 fn multi_digit_marker_under_indent() {
1175 let content = "10. Item\n\n continuation\n";
1176 let warnings = check(content);
1177 assert_eq!(warnings.len(), 1);
1178 assert!(warnings[0].message.contains("4 spaces"));
1179 }
1180
1181 #[test]
1184 fn mkdocs_3space_ordered_warns() {
1185 let content = "1. Item\n\n continuation\n";
1187 let warnings = check_mkdocs(content);
1188 assert_eq!(warnings.len(), 1);
1189 assert!(warnings[0].message.contains("4 spaces"));
1190 assert!(warnings[0].message.contains("MkDocs"));
1191 }
1192
1193 #[test]
1194 fn mkdocs_4space_ordered_no_warning() {
1195 let content = "1. Item\n\n continuation\n";
1196 assert!(check_mkdocs(content).is_empty());
1197 }
1198
1199 #[test]
1200 fn mkdocs_unordered_2space_ok() {
1201 let content = "- Item\n\n continuation\n";
1203 assert!(check_mkdocs(content).is_empty());
1204 }
1205
1206 #[test]
1207 fn mkdocs_unordered_2space_warns() {
1208 let content = "- Item\n\n continuation\n";
1210 let warnings = check_mkdocs(content);
1211 assert_eq!(warnings.len(), 1);
1212 assert!(warnings[0].message.contains("4 spaces"));
1213 }
1214
1215 #[test]
1218 fn fix_unordered_indent() {
1219 let content = "- Item\n\n continuation\n";
1221 let fixed = fix(content);
1222 assert_eq!(fixed, "- Item\n\n continuation\n");
1223 }
1224
1225 #[test]
1226 fn fix_ordered_indent() {
1227 let content = "1. Item\n\n continuation\n";
1228 let fixed = fix(content);
1229 assert_eq!(fixed, "1. Item\n\n continuation\n");
1230 }
1231
1232 #[test]
1233 fn fix_mkdocs_indent() {
1234 let content = "1. Item\n\n continuation\n";
1235 let fixed = fix_mkdocs(content);
1236 assert_eq!(fixed, "1. Item\n\n continuation\n");
1237 }
1238
1239 #[test]
1242 fn nested_list_items_not_flagged() {
1243 let content = "- Parent\n\n - Child\n";
1244 assert!(check(content).is_empty());
1245 }
1246
1247 #[test]
1248 fn nested_list_zero_indent_is_new_paragraph() {
1249 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
1251 assert!(check(content).is_empty());
1252 }
1253
1254 #[test]
1255 fn nested_list_partial_indent_flagged() {
1256 let content = "- Parent\n - Child\n\n continuation of parent\n";
1258 let warnings = check(content);
1259 assert_eq!(warnings.len(), 1);
1260 assert!(warnings[0].message.contains("2 spaces"));
1261 }
1262
1263 #[test]
1266 fn code_block_correctly_indented_no_warning() {
1267 let content = "- Item\n\n ```\n code\n ```\n";
1269 assert!(check(content).is_empty());
1270 }
1271
1272 #[test]
1273 fn code_fence_under_indented_warns() {
1274 let content = "- Item\n\n ```\n code\n ```\n";
1278 let warnings = check(content);
1279 assert_eq!(warnings.len(), 1);
1280 assert_eq!(warnings[0].line, 3);
1281 }
1282
1283 #[test]
1284 fn code_fence_under_indented_ordered_mkdocs() {
1285 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
1288 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
1290 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1292 assert!(warnings[0].message.contains("4 spaces"));
1293 assert!(warnings[0].message.contains("MkDocs"));
1294 }
1295
1296 #[test]
1297 fn code_fence_tilde_under_indented() {
1298 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1299 let warnings = check(content);
1300 assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].line, 3);
1302 }
1303
1304 #[test]
1307 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
1308 let content = "- Item\n\n\ncontinuation\n";
1310 assert!(check(content).is_empty());
1311 }
1312
1313 #[test]
1314 fn multiple_blank_lines_partial_indent_flags() {
1315 let content = "- Item\n\n\n continuation\n";
1316 let warnings = check(content);
1317 assert_eq!(warnings.len(), 1);
1318 }
1319
1320 #[test]
1323 fn empty_item_no_warning() {
1324 let content = "- \n- Second\n";
1325 assert!(check(content).is_empty());
1326 }
1327
1328 #[test]
1331 fn multiple_items_mixed_indent() {
1332 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
1333 let warnings = check(content);
1334 assert_eq!(warnings.len(), 1);
1335 assert_eq!(warnings[0].line, 7);
1336 }
1337
1338 #[test]
1341 fn task_list_correct_indent() {
1342 let content = "- [ ] Task\n\n continuation\n";
1344 assert!(check(content).is_empty());
1345 }
1346
1347 #[test]
1350 fn frontmatter_not_flagged() {
1351 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
1352 assert!(check(content).is_empty());
1353 }
1354
1355 #[test]
1358 fn fix_multiple_items() {
1359 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
1360 let fixed = fix(content);
1361 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
1362 }
1363
1364 #[test]
1365 fn fix_multiline_loose_continuation_all_lines() {
1366 let content = "1. Item\n\n line one\n line two\n line three\n";
1367 let fixed = fix(content);
1368 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
1369 }
1370
1371 #[test]
1374 fn sibling_item_boundary_respected() {
1375 let content = "- First\n- Second\n\n continuation\n";
1377 assert!(check(content).is_empty());
1378 }
1379
1380 #[test]
1383 fn blockquote_list_correct_indent_no_warning() {
1384 let content = "> - Item\n>\n> continuation\n";
1387 assert!(check(content).is_empty());
1388 }
1389
1390 #[test]
1391 fn blockquote_list_under_indent_no_false_positive() {
1392 let content = "> - Item\n>\n> continuation\n";
1397 assert!(check(content).is_empty());
1398 }
1399
1400 #[test]
1403 fn deep_nesting_correct_indent() {
1404 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1405 assert!(check(content).is_empty());
1406 }
1407
1408 #[test]
1409 fn deep_nesting_under_indent() {
1410 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
1413 let warnings = check(content);
1414 assert_eq!(warnings.len(), 1);
1415 assert!(warnings[0].message.contains("6 spaces"));
1416 assert!(warnings[0].message.contains("found 5"));
1417 }
1418
1419 #[test]
1420 fn deep_nesting_middle_level_continuation_bullets() {
1421 let content = "- L1\n - L2\n - L3\n\n continuation of L2\n";
1425 assert!(check(content).is_empty());
1426 }
1427
1428 #[test]
1429 fn deep_nesting_middle_level_continuation_ordered() {
1430 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";
1433 assert!(check(content).is_empty());
1434 }
1435
1436 #[test]
1437 fn deep_nesting_outermost_continuation() {
1438 let content = "- L1\n - L2\n - L3\n\n continuation of L1\n";
1441 assert!(check(content).is_empty());
1442 }
1443
1444 #[test]
1445 fn deep_nesting_between_levels_still_flagged() {
1446 let content = "- L1\n - L2\n - L3\n\n continuation\n";
1449 let warnings = check(content);
1450 assert_eq!(warnings.len(), 1);
1451 assert!(warnings[0].message.contains("4 spaces"));
1452 assert!(warnings[0].message.contains("found 3"));
1453 }
1454
1455 #[test]
1456 fn deep_nesting_beyond_deepest_still_flagged() {
1457 let content = "- L1\n - L2\n - L3\n\n continuation\n";
1459 let warnings = check(content);
1460 assert_eq!(warnings.len(), 1);
1461 assert!(warnings[0].message.contains("over-indented"));
1462 assert!(warnings[0].message.contains("expected 6, found 7"));
1463 }
1464
1465 #[test]
1466 fn four_levels_middle_continuation() {
1467 let content = "- L1\n - L2\n - L3\n - L4\n\n continuation of L2\n";
1470 assert!(check(content).is_empty());
1471 }
1472
1473 #[test]
1474 fn nested_sibling_closes_deeper_level() {
1475 let content = "- L1\n - L2a\n - L3\n - L2b\n\n continuation of L2b\n";
1478 assert!(check(content).is_empty());
1479 }
1480
1481 #[test]
1482 fn deep_nesting_middle_level_continuation_fix_preserved() {
1483 let content = "- L1\n - L2\n - L3\n\n continuation of L2\n";
1485 assert_eq!(fix(content), content);
1486 }
1487
1488 #[test]
1491 fn loose_tab_continuation_over_indented() {
1492 let content = "- Item\n\n\tcontinuation\n";
1497 let warnings = check(content);
1498 assert_eq!(warnings.len(), 1);
1499 assert_eq!(warnings[0].line, 3);
1500 assert_eq!(fix(content), "- Item\n\n continuation\n");
1501 }
1502
1503 #[test]
1506 fn multiple_continuations_correct() {
1507 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
1508 assert!(check(content).is_empty());
1509 }
1510
1511 #[test]
1512 fn multiple_continuations_second_under_indent() {
1513 let content = "- Item\n\n para 1\n\n continuation 2\n";
1515 let warnings = check(content);
1516 assert_eq!(warnings.len(), 1);
1517 assert_eq!(warnings[0].line, 5);
1518 }
1519
1520 #[test]
1523 fn ordered_paren_marker_correct() {
1524 let content = "1) Item\n\n continuation\n";
1526 assert!(check(content).is_empty());
1527 }
1528
1529 #[test]
1530 fn ordered_paren_marker_under_indent() {
1531 let content = "1) Item\n\n continuation\n";
1532 let warnings = check(content);
1533 assert_eq!(warnings.len(), 1);
1534 assert!(warnings[0].message.contains("3 spaces"));
1535 }
1536
1537 #[test]
1540 fn star_marker_correct() {
1541 let content = "* Item\n\n continuation\n";
1542 assert!(check(content).is_empty());
1543 }
1544
1545 #[test]
1546 fn star_marker_under_indent() {
1547 let content = "* Item\n\n continuation\n";
1548 let warnings = check(content);
1549 assert_eq!(warnings.len(), 1);
1550 }
1551
1552 #[test]
1553 fn plus_marker_correct() {
1554 let content = "+ Item\n\n continuation\n";
1555 assert!(check(content).is_empty());
1556 }
1557
1558 #[test]
1561 fn heading_after_list_no_warning() {
1562 let content = "- Item\n\n# Heading\n";
1563 assert!(check(content).is_empty());
1564 }
1565
1566 #[test]
1569 fn hr_after_list_no_warning() {
1570 let content = "- Item\n\n---\n";
1571 assert!(check(content).is_empty());
1572 }
1573
1574 #[test]
1577 fn reference_link_def_not_flagged() {
1578 let content = "- Item\n\n [link]: https://example.com\n";
1579 assert!(check(content).is_empty());
1580 }
1581
1582 #[test]
1585 fn footnote_def_not_flagged() {
1586 let content = "- Item\n\n [^1]: footnote text\n";
1587 assert!(check(content).is_empty());
1588 }
1589
1590 #[test]
1591 fn footnote_multiline_body_after_list_not_flagged() {
1592 let content = "# A list followed by a footnote\n\n\
1596 Here is a paragraph.[^fn]\n\n\
1597 - This is a list.\n\n\
1598 [^fn]:\n\
1599 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1600 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1601 assert!(check(content).is_empty());
1602 }
1603
1604 #[test]
1605 fn fix_footnote_multiline_body_after_list_is_noop() {
1606 let content = "# A list followed by a footnote\n\n\
1610 Here is a paragraph.[^fn]\n\n\
1611 - This is a list.\n\n\
1612 [^fn]:\n\
1613 \x20\x20\x20\x20Here is a footnote that spans multiple lines.\n\
1614 \x20\x20\x20\x20It should thus be indented by at least four spaces.\n";
1615 assert_eq!(fix(content), content);
1616 }
1617
1618 #[test]
1619 fn footnote_body_indented_past_list_content_col_not_flagged() {
1620 let content = "- Item\n\n[^fn]:\n Body line one.\n Body line two.\n";
1624 assert!(check(content).is_empty());
1625 }
1626
1627 #[test]
1628 fn list_inside_footnote_body_continuation_not_flagged() {
1629 let content = "Text.[^fn]\n\n[^fn]:\n\
1633 \x20\x20\x20\x20- nested item\n\
1634 \x20\x20\x20\x20\x20\x20\x20over-indented continuation\n";
1635 assert!(check(content).is_empty());
1636 }
1637
1638 #[test]
1639 fn footnote_multiline_body_after_list_not_flagged_mkdocs() {
1640 let content = "Here is a paragraph.[^fn]\n\n\
1644 - This is a list.\n\n\
1645 [^fn]:\n\
1646 \x20\x20\x20\x20\x20\x20Footnote body that spans\n\
1647 \x20\x20\x20\x20\x20\x20multiple indented lines.\n";
1648 assert!(check_mkdocs(content).is_empty());
1649 }
1650
1651 #[test]
1654 fn fix_deep_nesting() {
1655 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
1656 let fixed = fix(content);
1657 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
1658 }
1659
1660 #[test]
1661 fn fix_mkdocs_unordered() {
1662 let content = "- Item\n\n continuation\n";
1664 let fixed = fix_mkdocs(content);
1665 assert_eq!(fixed, "- Item\n\n continuation\n");
1666 }
1667
1668 #[test]
1669 fn fix_code_fence_indent() {
1670 let content = "- Item\n\n ```\n code\n ```\n";
1673 let fixed = fix(content);
1674 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
1675 }
1676
1677 #[test]
1678 fn fix_mkdocs_code_fence_indent() {
1679 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
1681 let fixed = fix_mkdocs(content);
1682 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
1683 }
1684
1685 #[test]
1688 fn empty_document_no_warning() {
1689 assert!(check("").is_empty());
1690 }
1691
1692 #[test]
1693 fn whitespace_only_no_warning() {
1694 assert!(check(" \n\n \n").is_empty());
1695 }
1696
1697 #[test]
1700 fn no_list_no_warning() {
1701 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
1702 assert!(check(content).is_empty());
1703 }
1704
1705 #[test]
1708 fn multiline_continuation_all_lines_flagged() {
1709 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";
1710 let warnings = check(content);
1711 assert_eq!(warnings.len(), 3);
1712 assert_eq!(warnings[0].line, 3);
1713 assert_eq!(warnings[1].line, 4);
1714 assert_eq!(warnings[2].line, 5);
1715 }
1716
1717 #[test]
1718 fn multiline_continuation_with_frontmatter_fix() {
1719 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";
1720 let fixed = fix(content);
1721 assert_eq!(
1722 fixed,
1723 "---\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"
1724 );
1725 }
1726
1727 #[test]
1728 fn multiline_continuation_correct_indent_no_warning() {
1729 let content = "1. Item\n\n line one\n line two\n line three\n";
1730 assert!(check(content).is_empty());
1731 }
1732
1733 #[test]
1734 fn multiline_continuation_mixed_indent() {
1735 let content = "1. Item\n\n correct\n wrong\n correct\n";
1736 let warnings = check(content);
1737 assert_eq!(warnings.len(), 1);
1738 assert_eq!(warnings[0].line, 4);
1739 }
1740
1741 #[test]
1742 fn multiline_continuation_unordered() {
1743 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
1744 let warnings = check(content);
1745 assert_eq!(warnings.len(), 3);
1746 let fixed = fix(content);
1747 assert_eq!(
1748 fixed,
1749 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
1750 );
1751 }
1752
1753 #[test]
1754 fn multiline_continuation_two_items_fix() {
1755 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
1756 let fixed = fix(content);
1757 assert_eq!(
1758 fixed,
1759 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1760 );
1761 }
1762
1763 #[test]
1764 fn fence_fix_does_not_break_pairing_for_md031() {
1765 let content = "#### title\n\nabc\n\n\
1772 1. ab\n\n\
1773 \x20\x20`aabbccdd`\n\n\
1774 2. cd\n\n\
1775 \x20\x20`bbcc dd ee`\n\n\
1776 \x20\x20```\n\
1777 \x20\x20abcd\n\
1778 \x20\x20ef gh\n\
1779 \x20\x20```\n\n\
1780 \x20\x20uu\n\n\
1781 \x20\x20```\n\
1782 \x20\x20cdef\n\
1783 \x20\x20gh ij\n\
1784 \x20\x20```\n";
1785 let expected = "#### title\n\nabc\n\n\
1786 1. ab\n\n\
1787 \x20\x20\x20`aabbccdd`\n\n\
1788 2. cd\n\n\
1789 \x20\x20\x20`bbcc dd ee`\n\n\
1790 \x20\x20\x20```\n\
1791 \x20\x20\x20abcd\n\
1792 \x20\x20\x20ef gh\n\
1793 \x20\x20\x20```\n\n\
1794 \x20\x20\x20uu\n\n\
1795 \x20\x20\x20```\n\
1796 \x20\x20\x20cdef\n\
1797 \x20\x20\x20gh ij\n\
1798 \x20\x20\x20```\n";
1799 assert_eq!(fix(content), expected);
1800 }
1801
1802 #[test]
1803 fn multiline_continuation_separated_by_blank() {
1804 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1805 let warnings = check(content);
1806 assert_eq!(warnings.len(), 4);
1807 let fixed = fix(content);
1808 assert_eq!(
1809 fixed,
1810 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1811 );
1812 }
1813
1814 #[test]
1815 fn tab_indented_fence_is_normalized_to_spaces() {
1816 let content = "100. ab\n\n\t```\n\tabcd\n\t```\n";
1824 let expected = "100. ab\n\n ```\n abcd\n ```\n";
1825 assert_eq!(fix(content), expected);
1826 }
1827
1828 #[test]
1837 fn loose_continuation_over_indented_flagged() {
1838 let content = "* Item\n\n over-indented\n";
1841 let warnings = check(content);
1842 assert_eq!(warnings.len(), 1);
1843 assert_eq!(warnings[0].line, 3);
1844 assert!(warnings[0].message.contains("over-indented"));
1845 assert!(warnings[0].message.contains("expected 2"));
1846 assert!(warnings[0].message.contains("found 3"));
1847 }
1848
1849 #[test]
1850 fn loose_continuation_over_indented_multiline_mixed() {
1851 let content = "* Item\n\n over one\n correct\n over two\n";
1853 let warnings = check(content);
1854 assert_eq!(warnings.len(), 2);
1855 assert_eq!(warnings[0].line, 3);
1856 assert_eq!(warnings[1].line, 5);
1857 }
1858
1859 #[test]
1860 fn fix_loose_continuation_over_indented() {
1861 let content = "* Item\n\n over one\n correct\n over two\n";
1862 let fixed = fix(content);
1863 assert_eq!(fixed, "* Item\n\n over one\n correct\n over two\n");
1864 }
1865
1866 #[test]
1867 fn fix_tight_and_loose_items_normalized_identically() {
1868 let content = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1871 * 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\
1872 * 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";
1873 let expected = "---\ntitle: Heading\n---\n\nSome introductory text:\n\n\
1874 * 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\
1875 * 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";
1876 assert_eq!(fix(content), expected);
1877 }
1878
1879 #[test]
1880 fn multi_paragraph_item_loose_paragraph_over_indented() {
1881 let content = "* Item.\n tight over\n\n loose over\n";
1884 let warnings = check(content);
1885 assert_eq!(warnings.len(), 2);
1886 assert_eq!(warnings[0].line, 2);
1887 assert_eq!(warnings[1].line, 4);
1888 }
1889
1890 #[test]
1891 fn loose_indented_code_block_not_flagged() {
1892 let content = "- Item\n\n code line\n";
1896 assert!(check(content).is_empty());
1897 }
1898
1899 #[test]
1900 fn mkdocs_loose_over_indented_flagged() {
1901 let content = "1. Item\n\n over\n";
1904 let warnings = check_mkdocs(content);
1905 assert_eq!(warnings.len(), 1);
1906 assert_eq!(warnings[0].line, 3);
1907 assert!(warnings[0].message.contains("over-indented"));
1908 assert!(warnings[0].message.contains("expected 4"));
1909 assert!(warnings[0].message.contains("found 5"));
1910 }
1911
1912 #[test]
1913 fn task_list_loose_over_indented_flagged() {
1914 let content = "- [ ] Task\n\n over\n";
1917 let warnings = check(content);
1918 assert_eq!(warnings.len(), 1);
1919 assert_eq!(warnings[0].line, 3);
1920 }
1921
1922 #[test]
1923 fn loose_over_indent_boundary_below_code_block_threshold_flagged() {
1924 let content = "- Item\n\n over\n";
1929 let warnings = check(content);
1930 assert_eq!(warnings.len(), 1);
1931 assert_eq!(warnings[0].line, 3);
1932 assert!(warnings[0].message.contains("expected 2"));
1933 assert!(warnings[0].message.contains("found 5"));
1934 }
1935
1936 #[test]
1937 fn loose_over_indent_does_not_steal_nested_under_indent() {
1938 let content = "- Outer\n - Inner\n\n continuation\n";
1945 let warnings = check(content);
1946 assert_eq!(warnings.len(), 1);
1947 assert_eq!(warnings[0].line, 4);
1948 assert!(warnings[0].message.contains("4 spaces"));
1949 assert!(warnings[0].message.contains("found 3"));
1950 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1951 }
1952
1953 #[test]
1954 fn loose_over_indent_attributes_to_deepest_enclosing_item() {
1955 let content = "- Outer\n - Inner\n\n continuation\n";
1959 let warnings = check(content);
1960 assert_eq!(warnings.len(), 1);
1961 assert_eq!(warnings[0].line, 4);
1962 assert!(warnings[0].message.contains("expected 4"));
1963 assert!(warnings[0].message.contains("found 5"));
1964 assert_eq!(fix(content), "- Outer\n - Inner\n\n continuation\n");
1965 }
1966
1967 #[test]
1976 fn loose_over_indented_fence_not_flagged() {
1977 let content = "- Item\n\n ```\n code\n ```\n";
1978 assert!(check(content).is_empty());
1979 assert_eq!(fix(content), content);
1980 }
1981
1982 #[test]
1983 fn tight_over_indented_fence_not_flagged() {
1984 let content = "- Item\n ```\n code\n ```\n";
1985 assert!(check(content).is_empty());
1986 assert_eq!(fix(content), content);
1987 }
1988
1989 #[test]
1990 fn over_indented_tilde_fence_not_flagged() {
1991 let content = "- Item\n\n ~~~\n code\n ~~~\n";
1992 assert!(check(content).is_empty());
1993 assert_eq!(fix(content), content);
1994 }
1995
1996 #[test]
1997 fn fence_like_code_content_inside_fenced_block_not_flagged() {
1998 let content = "- Item\n\n ~~~\n ```\n ~~~\n";
2001 assert!(check(content).is_empty());
2002 assert_eq!(fix(content), content);
2003 }
2004
2005 #[test]
2006 fn unterminated_over_indented_fence_not_flagged() {
2007 let content = "- Item\n\n ```\n code1\n code2deeper\n";
2010 assert!(check(content).is_empty());
2011 assert_eq!(fix(content), content);
2012 }
2013
2014 #[test]
2022 fn task_list_tight_continuation_post_checkbox_reproducer_579() {
2023 let content = "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n";
2026 assert!(check(content).is_empty());
2027 }
2028
2029 #[test]
2030 fn task_list_tight_continuation_dash_unchecked() {
2031 let content = "- [ ] Task\n continuation\n";
2032 assert!(check(content).is_empty());
2033 }
2034
2035 #[test]
2036 fn task_list_tight_continuation_dash_checked_lower() {
2037 let content = "- [x] Task\n continuation\n";
2038 assert!(check(content).is_empty());
2039 }
2040
2041 #[test]
2042 fn task_list_tight_continuation_dash_checked_upper() {
2043 let content = "- [X] Task\n continuation\n";
2044 assert!(check(content).is_empty());
2045 }
2046
2047 #[test]
2048 fn task_list_tight_continuation_star_marker() {
2049 let content = "* [ ] Task\n continuation\n";
2050 assert!(check(content).is_empty());
2051 }
2052
2053 #[test]
2054 fn task_list_tight_continuation_plus_marker() {
2055 let content = "+ [ ] Task\n continuation\n";
2056 assert!(check(content).is_empty());
2057 }
2058
2059 #[test]
2060 fn task_list_tight_continuation_content_column_still_valid() {
2061 let content = "- [ ] Task\n continuation\n";
2064 assert!(check(content).is_empty());
2065 }
2066
2067 #[test]
2068 fn task_list_tight_continuation_between_columns_still_flagged() {
2069 let content = "- [ ] Task\n continuation\n";
2072 let warnings = check(content);
2073 assert_eq!(warnings.len(), 1);
2074 assert!(warnings[0].message.contains("expected 2 or 6"));
2076 assert!(warnings[0].message.contains("found 4"));
2077 }
2078
2079 #[test]
2080 fn task_list_tight_continuation_overshoot_still_flagged() {
2081 let content = "- [ ] Task\n continuation\n";
2083 let warnings = check(content);
2084 assert_eq!(warnings.len(), 1);
2085 assert!(warnings[0].message.contains("expected 2 or 6"));
2086 assert!(warnings[0].message.contains("found 7"));
2087 }
2088
2089 #[test]
2092 fn fix_task_list_overshoot_snaps_to_task_col() {
2093 let content = "- [ ] Task\n continuation\n";
2097 let fixed = fix(content);
2098 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2099 }
2100
2101 #[test]
2102 fn fix_task_list_col_5_snaps_to_task_col() {
2103 let content = "- [ ] Task\n continuation\n";
2105 let fixed = fix(content);
2106 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2107 }
2108
2109 #[test]
2110 fn fix_task_list_col_3_snaps_to_content_col() {
2111 let content = "- [ ] Task\n continuation\n";
2113 let fixed = fix(content);
2114 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2115 }
2116
2117 #[test]
2118 fn fix_task_list_col_4_ties_to_content_col() {
2119 let content = "- [ ] Task\n continuation\n";
2124 let fixed = fix(content);
2125 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2126 }
2127
2128 #[test]
2129 fn fix_task_list_ordered_overshoot_snaps_to_task_col() {
2130 let content = "1. [ ] Task\n continuation\n";
2133 let fixed = fix(content);
2134 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2135 }
2136
2137 #[test]
2138 fn fix_task_list_ordered_under_overshoot_snaps_to_content_col() {
2139 let content = "1. [ ] Task\n continuation\n";
2142 let fixed = fix(content);
2143 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2144 }
2145
2146 #[test]
2147 fn task_list_tight_continuation_ordered_single_digit() {
2148 let content = "1. [ ] Task\n continuation\n";
2150 assert!(check(content).is_empty());
2151 }
2152
2153 #[test]
2154 fn task_list_tight_continuation_ordered_multi_digit() {
2155 let content = "10. [ ] Task\n continuation\n";
2157 assert!(check(content).is_empty());
2158 }
2159
2160 #[test]
2161 fn task_list_tight_continuation_nested_dash() {
2162 let content = "- Parent\n - [ ] Nested task\n continuation\n";
2164 assert!(check(content).is_empty());
2165 }
2166
2167 #[test]
2168 fn task_list_loose_continuation_post_checkbox_column_not_flagged() {
2169 let content = "- [ ] Task\n\n continuation\n";
2174 assert!(check(content).is_empty());
2175 }
2176
2177 #[test]
2178 fn task_list_empty_body_is_not_a_task() {
2179 let content = "- [ ]\n continuation\n";
2185 let warnings = check(content);
2186 assert_eq!(warnings.len(), 1);
2187 assert!(warnings[0].message.contains("found 4"));
2188 }
2189
2190 #[test]
2191 fn task_list_malformed_checkbox_is_not_a_task() {
2192 let content = "- [~] Not a task\n continuation\n";
2194 let warnings = check(content);
2195 assert_eq!(warnings.len(), 1);
2196 }
2197
2198 #[test]
2205 fn task_list_mkdocs_unordered_required_min_valid() {
2206 let content = "- [ ] Task\n continuation\n";
2208 assert!(check_mkdocs(content).is_empty());
2209 }
2210
2211 #[test]
2212 fn task_list_mkdocs_unordered_post_checkbox_valid() {
2213 let content = "- [ ] Task\n continuation\n";
2214 assert!(check_mkdocs(content).is_empty());
2215 }
2216
2217 #[test]
2218 fn task_list_mkdocs_unordered_between_flagged() {
2219 let content = "- [ ] Task\n continuation\n";
2221 let warnings = check_mkdocs(content);
2222 assert_eq!(warnings.len(), 1);
2223 }
2224
2225 #[test]
2226 fn task_list_mkdocs_ordered_both_columns_valid() {
2227 let at_4 = "1. [ ] Task\n continuation\n";
2229 assert!(check_mkdocs(at_4).is_empty());
2230 let at_7 = "1. [ ] Task\n continuation\n";
2231 assert!(check_mkdocs(at_7).is_empty());
2232 }
2233
2234 #[test]
2235 fn task_list_mkdocs_ordered_between_flagged() {
2236 let at_5 = "1. [ ] Task\n continuation\n";
2238 assert_eq!(check_mkdocs(at_5).len(), 1);
2239 let at_6 = "1. [ ] Task\n continuation\n";
2240 assert_eq!(check_mkdocs(at_6).len(), 1);
2241 }
2242
2243 #[test]
2253 fn fix_task_list_tie_sibling_at_task_col_snaps_to_task_col() {
2254 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
2258 let fixed = fix(content);
2259 assert_eq!(
2260 fixed,
2261 "- [ ] Task\n aligned continuation\n tied continuation\n"
2262 );
2263 }
2264
2265 #[test]
2266 fn fix_task_list_tie_sibling_at_content_col_snaps_to_content_col() {
2267 let content = "- [ ] Task\n aligned continuation\n tied continuation\n";
2270 let fixed = fix(content);
2271 assert_eq!(fixed, "- [ ] Task\n aligned continuation\n tied continuation\n");
2272 }
2273
2274 #[test]
2275 fn fix_task_list_tie_both_siblings_snaps_to_content_col() {
2276 let content = "- [ ] Task\n at content col\n at task col\n tied continuation\n";
2280 let fixed = fix(content);
2281 assert_eq!(
2282 fixed,
2283 "- [ ] Task\n at content col\n at task col\n tied continuation\n"
2284 );
2285 }
2286
2287 #[test]
2288 fn fix_task_list_tie_sees_task_col_through_tight_lazy_continuation() {
2289 let content = concat!("- [ ] Task\n", "lazy\n", " aligned at task col\n", " tied\n",);
2303 let fixed = fix(content);
2304 assert!(
2305 fixed.contains("\n tied\n"),
2306 "tied line should snap to col 6 (task col) because a task-col \
2307 sibling is visible past the tight lazy-continuation line; got:\n{fixed}"
2308 );
2309 }
2310
2311 #[test]
2318 fn task_list_tab_indented_continuation_flagged() {
2319 let content = "- [ ] Task\n\t\twrap\n";
2322 let warnings = check(content);
2323 assert_eq!(warnings.len(), 1);
2324 assert!(warnings[0].message.contains("expected 2 or 6"));
2325 assert!(warnings[0].message.contains("found 8"));
2326 }
2327
2328 #[test]
2329 fn fix_task_list_tab_indented_snaps_to_task_col() {
2330 let content = "- [ ] Task\n\t\twrap\n";
2332 let fixed = fix(content);
2333 assert_eq!(fixed, "- [ ] Task\n wrap\n");
2334 }
2335
2336 #[test]
2337 fn fix_task_list_single_tab_equidistant_snaps_to_content_col() {
2338 let content = "- [ ] Task\n\twrap\n";
2341 let fixed = fix(content);
2342 assert_eq!(fixed, "- [ ] Task\n wrap\n");
2343 }
2344
2345 #[test]
2355 fn task_list_blockquote_post_checkbox_not_flagged() {
2356 let content = "> - [ ] Task\n> continuation\n";
2358 assert!(check(content).is_empty());
2359 }
2360
2361 #[test]
2362 fn task_list_blockquote_between_cols_documented_limitation() {
2363 let content = "> - [ ] Task\n> continuation\n";
2367 assert!(check(content).is_empty());
2368 }
2369
2370 #[test]
2371 fn task_list_blockquote_overshoot_documented_limitation() {
2372 let content = "> - [ ] Task\n> continuation\n";
2374 assert!(check(content).is_empty());
2375 }
2376
2377 #[test]
2384 fn fix_task_list_mkdocs_unordered_overshoot_snaps_to_task_col() {
2385 let content = "- [ ] Task\n continuation\n";
2388 let fixed = fix_mkdocs(content);
2389 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2390 }
2391
2392 #[test]
2393 fn fix_task_list_mkdocs_unordered_tie_snaps_to_required() {
2394 let content = "- [ ] Task\n continuation\n";
2397 let fixed = fix_mkdocs(content);
2398 assert_eq!(fixed, "- [ ] Task\n continuation\n");
2399 }
2400
2401 #[test]
2402 fn fix_task_list_mkdocs_ordered_overshoot_snaps_to_task_col() {
2403 let content = "1. [ ] Task\n continuation\n";
2406 let fixed = fix_mkdocs(content);
2407 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2408 }
2409
2410 #[test]
2411 fn fix_task_list_mkdocs_ordered_near_required_snaps_to_required() {
2412 let content = "1. [ ] Task\n continuation\n";
2418 let fixed = fix_mkdocs(content);
2419 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2420 }
2421
2422 #[test]
2423 fn fix_task_list_mkdocs_ordered_between_cols_snaps_to_task_col() {
2424 let content = "1. [ ] Task\n continuation\n";
2427 let fixed = fix_mkdocs(content);
2428 assert_eq!(fixed, "1. [ ] Task\n continuation\n");
2429 }
2430
2431 fn assert_idempotent(content: &str) {
2441 let once = fix(content);
2442 let twice = fix(&once);
2443 assert_eq!(once, twice, "MD077 fix was not idempotent on input: {content:?}");
2444 }
2445
2446 fn assert_idempotent_mkdocs(content: &str) {
2447 let once = fix_mkdocs(content);
2448 let twice = fix_mkdocs(&once);
2449 assert_eq!(
2450 once, twice,
2451 "MD077 (MkDocs) fix was not idempotent on input: {content:?}"
2452 );
2453 }
2454
2455 #[test]
2456 fn idempotent_task_list_between_cols() {
2457 assert_idempotent("- [ ] Task\n continuation\n");
2458 }
2459
2460 #[test]
2461 fn idempotent_task_list_overshoot() {
2462 assert_idempotent("- [ ] Task\n continuation\n");
2463 }
2464
2465 #[test]
2466 fn idempotent_task_list_under_post_checkbox() {
2467 assert_idempotent("- [ ] Task\n continuation\n");
2468 }
2469
2470 #[test]
2471 fn idempotent_task_list_near_post_checkbox() {
2472 assert_idempotent("- [ ] Task\n continuation\n");
2473 }
2474
2475 #[test]
2476 fn idempotent_task_list_tab_overshoot() {
2477 assert_idempotent("- [ ] Task\n\t\twrap\n");
2478 }
2479
2480 #[test]
2481 fn idempotent_task_list_single_tab() {
2482 assert_idempotent("- [ ] Task\n\twrap\n");
2483 }
2484
2485 #[test]
2486 fn idempotent_task_list_ordered_overshoot() {
2487 assert_idempotent("1. [ ] Task\n continuation\n");
2488 }
2489
2490 #[test]
2491 fn idempotent_task_list_ordered_under() {
2492 assert_idempotent("1. [ ] Task\n continuation\n");
2493 }
2494
2495 #[test]
2496 fn idempotent_task_list_tie_with_sibling_at_task_col() {
2497 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2498 }
2499
2500 #[test]
2501 fn idempotent_task_list_tie_with_sibling_at_content_col() {
2502 assert_idempotent("- [ ] Task\n aligned\n tied\n");
2503 }
2504
2505 #[test]
2506 fn idempotent_task_list_mkdocs_unordered_overshoot() {
2507 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2508 }
2509
2510 #[test]
2511 fn idempotent_task_list_mkdocs_unordered_tie() {
2512 assert_idempotent_mkdocs("- [ ] Task\n continuation\n");
2513 }
2514
2515 #[test]
2516 fn idempotent_task_list_mkdocs_ordered_overshoot() {
2517 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2518 }
2519
2520 #[test]
2521 fn idempotent_task_list_mkdocs_ordered_between() {
2522 assert_idempotent_mkdocs("1. [ ] Task\n continuation\n");
2523 }
2524
2525 #[test]
2526 fn idempotent_task_list_reproducer_579() {
2527 assert_idempotent(
2531 "- [ ] Lorem ipsum dolor sit amet, consectetur adipiscing\n tempor incididunt ut labore.\n",
2532 );
2533 }
2534
2535 #[test]
2536 fn idempotent_non_task_list_still_holds() {
2537 assert_idempotent("1. Item\n over-indented\n");
2540 assert_idempotent("- Item\n\n continuation\n");
2541 }
2542
2543 #[test]
2550 fn idempotent_non_task_loose_under_indent_ordered() {
2551 assert_idempotent("1. Item\n\n continuation\n");
2553 }
2554
2555 #[test]
2556 fn idempotent_non_task_loose_under_indent_multi_digit() {
2557 assert_idempotent("10. Item\n\n continuation\n");
2559 }
2560
2561 #[test]
2562 fn idempotent_non_task_tight_over_indent_ordered() {
2563 assert_idempotent("1. Item\n over-indented\n");
2565 }
2566
2567 #[test]
2575 fn idempotent_non_task_fence_ordered_loose() {
2576 assert_idempotent("1. Item\n\n ```rust\n let x = 1;\n ```\n");
2578 }
2579
2580 #[test]
2581 fn idempotent_non_task_fence_tilde_under_indent() {
2582 assert_idempotent("1. Item\n\n ~~~\nplain text\n ~~~\n");
2588 }
2589
2590 #[test]
2591 fn idempotent_non_task_fence_interior_above_required() {
2592 assert_idempotent("1. Item\n\n ```\n deeply indented code\n ```\n");
2596 }
2597
2598 #[test]
2599 fn fence_fix_promotes_interior_below_scope_in_single_pass() {
2600 let content = "1. Item\n\n ```\ncode\n ```\n";
2604 let fixed = fix(content);
2605 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2606 }
2607
2608 #[test]
2609 fn fence_fix_preserves_interior_above_required() {
2610 let content = "1. Item\n\n ```\n code\n ```\n";
2613 let fixed = fix(content);
2614 assert_eq!(fixed, "1. Item\n\n ```\n code\n ```\n");
2615 }
2616
2617 #[test]
2624 fn idempotent_non_task_mkdocs_ordered_at_3_spaces() {
2625 assert_idempotent_mkdocs("1. Item\n\n continuation\n");
2627 }
2628
2629 #[test]
2630 fn idempotent_non_task_mkdocs_unordered_at_2_spaces() {
2631 assert_idempotent_mkdocs("- Item\n\n continuation\n");
2633 }
2634
2635 #[test]
2636 fn idempotent_non_task_mkdocs_fence_compound() {
2637 assert_idempotent_mkdocs("1. Item\n\n ```toml\n k = 1\n ```\n");
2639 }
2640
2641 #[test]
2644 fn aligned_tight_zero_indent_continuation_flagged() {
2645 let content = "- this is a long line\nthat continues on a second line\n";
2649 let warnings = check_aligned(content);
2650 assert_eq!(warnings.len(), 1);
2651 assert_eq!(warnings[0].line, 2);
2652 assert_eq!(
2653 fix_aligned(content),
2654 "- this is a long line\n that continues on a second line\n"
2655 );
2656 }
2657
2658 #[test]
2659 fn aligned_full_issue_example_made_consistent() {
2660 let content = "- this is a long line\n\
2663 that continues on a second line\n\
2664 - this is another long line\n\
2665 \x20\x20that continues on the next line\n\
2666 - yet again a long line\n\
2667 and still inconsistently spaced\n\
2668 \x20\x20and even worse\n";
2669 let expected = "- this is a long line\n\
2670 \x20\x20that continues on a second line\n\
2671 - this is another long line\n\
2672 \x20\x20that continues on the next line\n\
2673 - yet again a long line\n\
2674 \x20\x20and still inconsistently spaced\n\
2675 \x20\x20and even worse\n";
2676 assert_eq!(fix_aligned(content), expected);
2677 assert_eq!(fix_aligned(expected), expected);
2679 }
2680
2681 #[test]
2682 fn aligned_already_aligned_not_flagged() {
2683 let content = "- item\n continuation at content column\n";
2684 assert!(check_aligned(content).is_empty());
2685 }
2686
2687 #[test]
2688 fn aligned_tight_partial_indent_flagged() {
2689 let content = "- item\n continuation\n";
2691 let warnings = check_aligned(content);
2692 assert_eq!(warnings.len(), 1);
2693 assert_eq!(fix_aligned(content), "- item\n continuation\n");
2694 }
2695
2696 #[test]
2697 fn aligned_post_blank_zero_indent_still_new_paragraph() {
2698 let content = "- item\n\nnew paragraph\n";
2701 assert!(check_aligned(content).is_empty());
2702 assert_eq!(fix_aligned(content), content);
2703 }
2704
2705 #[test]
2708 fn aligned_top_level_blockquote_after_list_untouched() {
2709 let content = "- item\n> quote\n";
2713 assert!(check_aligned(content).is_empty());
2714 assert_eq!(fix_aligned(content), content);
2715 }
2716
2717 #[test]
2718 fn aligned_top_level_fence_after_list_untouched() {
2719 let content = "- item\n```\ncode\n```\n";
2720 assert!(check_aligned(content).is_empty());
2721 assert_eq!(fix_aligned(content), content);
2722 }
2723
2724 #[test]
2725 fn aligned_top_level_table_after_list_untouched() {
2726 let content = "- item\n| a | b |\n|---|---|\n| 1 | 2 |\n";
2727 assert!(check_aligned(content).is_empty());
2728 assert_eq!(fix_aligned(content), content);
2729 }
2730
2731 #[test]
2734 fn aligned_nested_tight_lazy_continuation_aligns_to_inner() {
2735 let content = "- Outer\n - Inner\ncontinuation\n";
2740 let warnings = check_aligned(content);
2741 assert_eq!(warnings.len(), 1);
2742 assert_eq!(fix_aligned(content), "- Outer\n - Inner\n continuation\n");
2743 }
2744
2745 #[test]
2746 fn aligned_nested_continuation_already_aligned_not_flagged() {
2747 let content = "- L1\n - L2\n cont of L2 at 4\n";
2748 assert!(check_aligned(content).is_empty());
2749 }
2750
2751 #[test]
2752 fn aligned_nested_idempotent() {
2753 let content = "- Outer\n - Inner\ncontinuation\n";
2754 let once = fix_aligned(content);
2755 assert_eq!(fix_aligned(&once), once);
2756 }
2757
2758 #[test]
2759 fn aligned_three_level_nesting_aligns_to_innermost() {
2760 let content = "- L1\n - L2\n - L3\ncont\n";
2763 assert_eq!(fix_aligned(content), "- L1\n - L2\n - L3\n cont\n");
2764 }
2765
2766 #[test]
2767 fn aligned_continuation_after_sibling_owned_by_last_item() {
2768 let content = "- a\n- b\nlazy\n";
2771 assert_eq!(fix_aligned(content), "- a\n- b\n lazy\n");
2772 }
2773
2774 #[test]
2775 fn aligned_multi_digit_ordered_marker_aligns_to_content_column() {
2776 let content = "10. Item\nwrap\n";
2777 assert_eq!(fix_aligned(content), "10. Item\n wrap\n");
2778 }
2779
2780 #[test]
2781 fn aligned_setext_heading_after_list_left_alone() {
2782 let content = "- item\nText\n===\n";
2785 assert!(check_aligned(content).is_empty());
2786 assert_eq!(fix_aligned(content), content);
2787 }
2788
2789 #[test]
2790 fn aligned_latent_marker_in_continuation_is_idempotent() {
2791 let content = "# \n- \n``\n2. \n![]()";
2797 let once = fix_aligned(content);
2798 assert_eq!(fix_aligned(&once), once, "fix must be idempotent in one pass");
2799 assert_eq!(once, content, "item with a latent marker is left untouched");
2800 }
2801
2802 #[test]
2803 fn aligned_latent_table_in_continuation_is_idempotent() {
2804 let content = "- \n![`]()\n| | ` |\n| --- | --- |";
2809 let once = fix_aligned(content);
2810 assert_eq!(fix_aligned(&once), once, "fix must be idempotent in one pass");
2811 assert_eq!(once, content, "item with a latent table is left untouched");
2812 }
2813
2814 #[test]
2815 fn aligned_blockquote_nested_list_not_touched() {
2816 let content = "> - item\n> wrap\n";
2820 assert!(check_aligned(content).is_empty());
2821 assert_eq!(fix_aligned(content), content);
2822 }
2823
2824 #[test]
2827 fn aligned_task_post_checkbox_column_accepted() {
2828 let content = "- [ ] Task\n wrap\n";
2831 assert!(check_aligned(content).is_empty());
2832 assert_eq!(fix_aligned(content), content);
2833 }
2834
2835 #[test]
2836 fn aligned_task_under_indent_snaps_to_content_column() {
2837 let content = "- [ ] Task\nwrap\n";
2838 let warnings = check_aligned(content);
2839 assert_eq!(warnings.len(), 1);
2840 assert_eq!(fix_aligned(content), "- [ ] Task\n wrap\n");
2841 }
2842
2843 #[test]
2846 fn aligned_mkdocs_tight_under_indent_snaps_to_four() {
2847 let content = "- item\nwrap\n";
2849 let warnings = check_aligned_mkdocs(content);
2850 assert_eq!(warnings.len(), 1);
2851 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
2852 assert_eq!(aligned_rule().fix(&ctx).unwrap(), "- item\n wrap\n");
2853 }
2854
2855 #[test]
2858 fn any_default_does_not_flag_tight_lazy_continuation() {
2859 let content = "- item\nwrapped at zero indent\n";
2861 assert!(check(content).is_empty());
2862 assert_eq!(fix(content), content);
2863 }
2864
2865 #[test]
2866 fn from_config_aligned_enables_tight_flagging() {
2867 let mut config = crate::config::Config::default();
2869 let mut rule_config = crate::config::RuleConfig::default();
2870 rule_config
2871 .values
2872 .insert("style".to_string(), toml::Value::String("aligned".to_string()));
2873 config.rules.insert("MD077".to_string(), rule_config);
2874
2875 let rule = MD077ListContinuationIndent::from_config(&config);
2876 let ctx = LintContext::new("- item\nwrap\n", MarkdownFlavor::Standard, None);
2877 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
2878 }
2879
2880 #[test]
2881 fn from_config_default_is_any() {
2882 let config = crate::config::Config::default();
2884 let rule = MD077ListContinuationIndent::from_config(&config);
2885 let ctx = LintContext::new("- item\nwrap\n", MarkdownFlavor::Standard, None);
2886 assert!(rule.check(&ctx).unwrap().is_empty());
2887 }
2888
2889 #[test]
2890 fn aligned_tight_underindented_fence_inside_item_left_alone() {
2891 let content = "- item\n ```\n code\n ```\n";
2895 assert!(check_aligned(content).is_empty());
2896 assert_eq!(fix_aligned(content), content);
2897 }
2898
2899 #[test]
2900 fn aligned_task_under_indent_fix_is_idempotent() {
2901 let content = "- [ ] Task\nwrap\n";
2902 let once = fix_aligned(content);
2903 assert_eq!(fix_aligned(&once), once);
2904 }
2905
2906 #[test]
2907 fn aligned_partial_indent_fix_is_idempotent() {
2908 let content = "- item\n continuation\n";
2909 let once = fix_aligned(content);
2910 assert_eq!(fix_aligned(&once), once);
2911 }
2912}