1use crate::lint_context::LintContext;
7use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
8
9#[derive(Clone, Default)]
20pub struct MD077ListContinuationIndent;
21
22impl MD077ListContinuationIndent {
23 fn is_block_level_construct(trimmed: &str) -> bool {
25 if trimmed.starts_with("[^") && trimmed.contains("]:") {
27 return true;
28 }
29 if trimmed.starts_with("*[") && trimmed.contains("]:") {
31 return true;
32 }
33 if trimmed.starts_with('[') && !trimmed.starts_with("[^") && trimmed.contains("]: ") {
36 return true;
37 }
38 false
39 }
40
41 fn is_code_fence(trimmed: &str) -> bool {
43 let bytes = trimmed.as_bytes();
44 if bytes.len() < 3 {
45 return false;
46 }
47 let ch = bytes[0];
48 (ch == b'`' || ch == b'~') && bytes[1] == ch && bytes[2] == ch
49 }
50
51 fn starts_with_list_marker(trimmed: &str) -> bool {
55 let bytes = trimmed.as_bytes();
56 match bytes.first() {
57 Some(b'*' | b'-' | b'+') => bytes.get(1).is_some_and(|&b| b == b' ' || b == b'\t'),
58 Some(b'0'..=b'9') => {
59 let rest = trimmed.trim_start_matches(|c: char| c.is_ascii_digit());
60 rest.starts_with(". ") || rest.starts_with(") ")
61 }
62 _ => false,
63 }
64 }
65
66 fn should_skip_line(info: &crate::lint_context::LineInfo, trimmed: &str) -> bool {
71 if info.in_code_block && !Self::is_code_fence(trimmed) {
72 return true;
73 }
74 info.in_front_matter
75 || info.in_html_block
76 || info.in_html_comment
77 || info.in_mdx_comment
78 || info.in_mkdocstrings
79 || info.in_esm_block
80 || info.in_math_block
81 || info.in_admonition
82 || info.in_content_tab
83 || info.in_pymdown_block
84 || info.in_definition_list
85 || info.in_mkdocs_html_markdown
86 || info.in_kramdown_extension_block
87 }
88}
89
90impl Rule for MD077ListContinuationIndent {
91 fn name(&self) -> &'static str {
92 "MD077"
93 }
94
95 fn description(&self) -> &'static str {
96 "List continuation content indentation"
97 }
98
99 fn check(&self, ctx: &LintContext) -> LintResult {
100 if ctx.content.is_empty() {
101 return Ok(Vec::new());
102 }
103
104 let strict_indent = ctx.flavor.requires_strict_list_indent();
105 let total_lines = ctx.lines.len();
106 let mut warnings = Vec::new();
107 let mut flagged_lines = std::collections::HashSet::new();
108
109 let mut items: Vec<(usize, usize, usize)> = Vec::new(); for block in &ctx.list_blocks {
114 for &item_line in &block.item_lines {
115 if let Some(info) = ctx.line_info(item_line)
116 && let Some(ref li) = info.list_item
117 {
118 items.push((item_line, li.marker_column, li.content_column));
119 }
120 }
121 }
122 items.sort_unstable();
123 items.dedup_by_key(|&mut (ln, _, _)| ln);
124
125 for (item_idx, &(item_line, marker_col, content_col)) in items.iter().enumerate() {
126 let required = if strict_indent { content_col.max(4) } else { content_col };
127
128 let range_end = items
131 .iter()
132 .skip(item_idx + 1)
133 .find(|&&(_, mc, _)| mc <= marker_col)
134 .map_or(total_lines, |&(ln, _, _)| ln - 1);
135
136 let mut saw_blank = false;
137 let mut nested_content_col: Option<usize> = None;
140
141 for line_num in (item_line + 1)..=range_end {
142 let Some(line_info) = ctx.line_info(line_num) else {
143 continue;
144 };
145
146 let trimmed = line_info.content(ctx.content).trim_start();
147
148 if Self::should_skip_line(line_info, trimmed) {
149 continue;
150 }
151
152 if line_info.is_blank {
153 saw_blank = true;
154 continue;
155 }
156
157 if let Some(ref li) = line_info.list_item {
159 if li.marker_column > marker_col {
160 nested_content_col = Some(li.content_column);
161 } else {
162 nested_content_col = None;
163 }
164 saw_blank = false;
165 continue;
166 }
167
168 if line_info.heading.is_some() {
170 break;
171 }
172
173 if line_info.is_horizontal_rule {
175 break;
176 }
177
178 if Self::is_block_level_construct(trimmed) {
181 continue;
182 }
183
184 let actual = line_info.visual_indent;
185
186 if let Some(ncc) = nested_content_col {
189 if actual >= ncc {
190 continue;
191 }
192 nested_content_col = None;
193 }
194
195 if !saw_blank {
197 if actual > required && !Self::starts_with_list_marker(trimmed) && flagged_lines.insert(line_num) {
198 let line_content = line_info.content(ctx.content);
199 let fix_start = line_info.byte_offset;
200 let fix_end = fix_start + line_info.indent;
201
202 warnings.push(LintWarning {
203 rule_name: Some("MD077".to_string()),
204 line: line_num,
205 column: 1,
206 end_line: line_num,
207 end_column: line_content.len() + 1,
208 message: format!("Continuation line over-indented (expected {required}, found {actual})",),
209 severity: Severity::Warning,
210 fix: Some(Fix {
211 range: fix_start..fix_end,
212 replacement: " ".repeat(required),
213 }),
214 });
215 }
216 continue;
217 }
218
219 if actual <= marker_col {
223 break;
224 }
225
226 if actual < required && flagged_lines.insert(line_num) {
227 let line_content = line_info.content(ctx.content);
228
229 let message = if strict_indent {
230 format!(
231 "Content inside list item needs {required} spaces of indentation \
232 for MkDocs compatibility (found {actual})",
233 )
234 } else {
235 format!(
236 "Content after blank line in list item needs {required} spaces of \
237 indentation to remain part of the list (found {actual})",
238 )
239 };
240
241 let fix_start = line_info.byte_offset;
243 let fix_end = fix_start + line_info.indent;
244
245 warnings.push(LintWarning {
246 rule_name: Some("MD077".to_string()),
247 line: line_num,
248 column: 1,
249 end_line: line_num,
250 end_column: line_content.len() + 1,
251 message,
252 severity: Severity::Warning,
253 fix: Some(Fix {
254 range: fix_start..fix_end,
255 replacement: " ".repeat(required),
256 }),
257 });
258 }
259
260 }
265 }
266
267 Ok(warnings)
268 }
269
270 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
271 let warnings = self.check(ctx)?;
272 let warnings =
273 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
274 if warnings.is_empty() {
275 return Ok(ctx.content.to_string());
276 }
277
278 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
280 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
281
282 let mut content = ctx.content.to_string();
283 for fix in fixes {
284 if fix.range.start <= content.len() && fix.range.end <= content.len() {
285 content.replace_range(fix.range, &fix.replacement);
286 }
287 }
288
289 Ok(content)
290 }
291
292 fn category(&self) -> RuleCategory {
293 RuleCategory::List
294 }
295
296 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
297 ctx.content.is_empty() || ctx.list_blocks.is_empty()
298 }
299
300 fn as_any(&self) -> &dyn std::any::Any {
301 self
302 }
303
304 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
305 where
306 Self: Sized,
307 {
308 Box::new(Self)
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::config::MarkdownFlavor;
316
317 fn check(content: &str) -> Vec<LintWarning> {
318 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
319 let rule = MD077ListContinuationIndent;
320 rule.check(&ctx).unwrap()
321 }
322
323 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
324 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
325 let rule = MD077ListContinuationIndent;
326 rule.check(&ctx).unwrap()
327 }
328
329 fn fix(content: &str) -> String {
330 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
331 let rule = MD077ListContinuationIndent;
332 rule.fix(&ctx).unwrap()
333 }
334
335 fn fix_mkdocs(content: &str) -> String {
336 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
337 let rule = MD077ListContinuationIndent;
338 rule.fix(&ctx).unwrap()
339 }
340
341 #[test]
344 fn tight_lazy_continuation_zero_indent_not_flagged() {
345 let content = "- Item\ncontinuation\n";
347 assert!(check(content).is_empty());
348 }
349
350 #[test]
351 fn tight_continuation_correct_indent_not_flagged() {
352 let content = "1. Item\n continuation\n";
354 assert!(check(content).is_empty());
355 }
356
357 #[test]
358 fn tight_continuation_over_indented_ordered() {
359 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
361 let warnings = check(content);
362 assert_eq!(warnings.len(), 1);
363 assert_eq!(warnings[0].line, 2);
364 assert!(warnings[0].message.contains("over-indented"));
365 }
366
367 #[test]
368 fn tight_continuation_over_indented_unordered() {
369 let content = "- Item\n over-indented\n";
371 let warnings = check(content);
372 assert_eq!(warnings.len(), 1);
373 assert_eq!(warnings[0].line, 2);
374 }
375
376 #[test]
377 fn tight_continuation_multiple_over_indented_lines() {
378 let content = "1. Item\n line one\n line two\n line three\n";
379 let warnings = check(content);
380 assert_eq!(warnings.len(), 3);
381 }
382
383 #[test]
384 fn tight_continuation_mixed_correct_and_over() {
385 let content = "1. Item\n correct\n over-indented\n correct again\n";
386 let warnings = check(content);
387 assert_eq!(warnings.len(), 1);
388 assert_eq!(warnings[0].line, 3);
389 }
390
391 #[test]
392 fn tight_continuation_nested_over_indented() {
393 let content = "- L1\n - L2\n over-indented continuation of L2\n";
395 let warnings = check(content);
396 assert_eq!(warnings.len(), 1);
397 assert_eq!(warnings[0].line, 3);
398 assert!(warnings[0].message.contains("expected 4"));
400 assert!(warnings[0].message.contains("found 5"));
401 }
402
403 #[test]
404 fn tight_continuation_nested_correct_indent_not_flagged() {
405 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
408 assert!(check(content).is_empty());
409 }
410
411 #[test]
412 fn fix_tight_continuation_nested_over_indented() {
413 let content = "- L1\n - L2\n over-indented continuation of L2\n";
415 let fixed = fix(content);
416 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
417 }
418
419 #[test]
420 fn tight_continuation_under_indented_not_flagged() {
421 let content = "1. Item\n under-indented\n";
424 assert!(check(content).is_empty());
425 }
426
427 #[test]
428 fn tight_continuation_tab_over_indented() {
429 let content = "- Item\n\tover-indented\n";
431 let warnings = check(content);
432 assert_eq!(warnings.len(), 1);
433 }
434
435 #[test]
436 fn fix_tight_continuation_over_indented_ordered() {
437 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
438 let fixed = fix(content);
439 assert_eq!(
440 fixed,
441 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
442 );
443 }
444
445 #[test]
446 fn fix_tight_continuation_over_indented_unordered() {
447 let content = "- Item\n over-indented\n";
448 let fixed = fix(content);
449 assert_eq!(fixed, "- Item\n over-indented\n");
450 }
451
452 #[test]
453 fn fix_tight_continuation_multiple_lines() {
454 let content = "1. Item\n line one\n line two\n";
455 let fixed = fix(content);
456 assert_eq!(fixed, "1. Item\n line one\n line two\n");
457 }
458
459 #[test]
460 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
461 let content = "1. Item\n continuation\n";
464 assert!(check_mkdocs(content).is_empty());
465 }
466
467 #[test]
468 fn tight_continuation_mkdocs_5space_ordered_flagged() {
469 let content = "1. Item\n over-indented\n";
471 let warnings = check_mkdocs(content);
472 assert_eq!(warnings.len(), 1);
473 assert!(warnings[0].message.contains("expected 4"));
474 assert!(warnings[0].message.contains("found 5"));
475 }
476
477 #[test]
478 fn fix_tight_continuation_mkdocs_over_indented() {
479 let content = "1. Item\n over-indented\n";
480 let fixed = fix_mkdocs(content);
481 assert_eq!(fixed, "1. Item\n over-indented\n");
482 }
483
484 #[test]
485 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
486 let content = "* Level 0\n * Level 1\n * Level 2\n";
489 assert!(check(content).is_empty());
490 }
491
492 #[test]
493 fn tight_continuation_ordered_marker_not_flagged() {
494 let content = "- Parent\n 1. Child item\n";
496 assert!(check(content).is_empty());
497 }
498
499 #[test]
502 fn unordered_correct_indent_no_warning() {
503 let content = "- Item\n\n continuation\n";
504 assert!(check(content).is_empty());
505 }
506
507 #[test]
508 fn unordered_partial_indent_warns() {
509 let content = "- Item\n\n continuation\n";
512 let warnings = check(content);
513 assert_eq!(warnings.len(), 1);
514 assert_eq!(warnings[0].line, 3);
515 assert!(warnings[0].message.contains("2 spaces"));
516 assert!(warnings[0].message.contains("found 1"));
517 }
518
519 #[test]
520 fn unordered_zero_indent_is_new_paragraph() {
521 let content = "- Item\n\ncontinuation\n";
524 assert!(check(content).is_empty());
525 }
526
527 #[test]
530 fn ordered_3space_correct_commonmark() {
531 let content = "1. Item\n\n continuation\n";
533 assert!(check(content).is_empty());
534 }
535
536 #[test]
537 fn ordered_2space_under_indent_commonmark() {
538 let content = "1. Item\n\n continuation\n";
539 let warnings = check(content);
540 assert_eq!(warnings.len(), 1);
541 assert!(warnings[0].message.contains("3 spaces"));
542 assert!(warnings[0].message.contains("found 2"));
543 }
544
545 #[test]
548 fn multi_digit_marker_correct() {
549 let content = "10. Item\n\n continuation\n";
551 assert!(check(content).is_empty());
552 }
553
554 #[test]
555 fn multi_digit_marker_under_indent() {
556 let content = "10. Item\n\n continuation\n";
557 let warnings = check(content);
558 assert_eq!(warnings.len(), 1);
559 assert!(warnings[0].message.contains("4 spaces"));
560 }
561
562 #[test]
565 fn mkdocs_3space_ordered_warns() {
566 let content = "1. Item\n\n continuation\n";
568 let warnings = check_mkdocs(content);
569 assert_eq!(warnings.len(), 1);
570 assert!(warnings[0].message.contains("4 spaces"));
571 assert!(warnings[0].message.contains("MkDocs"));
572 }
573
574 #[test]
575 fn mkdocs_4space_ordered_no_warning() {
576 let content = "1. Item\n\n continuation\n";
577 assert!(check_mkdocs(content).is_empty());
578 }
579
580 #[test]
581 fn mkdocs_unordered_2space_ok() {
582 let content = "- Item\n\n continuation\n";
584 assert!(check_mkdocs(content).is_empty());
585 }
586
587 #[test]
588 fn mkdocs_unordered_2space_warns() {
589 let content = "- Item\n\n continuation\n";
591 let warnings = check_mkdocs(content);
592 assert_eq!(warnings.len(), 1);
593 assert!(warnings[0].message.contains("4 spaces"));
594 }
595
596 #[test]
599 fn fix_unordered_indent() {
600 let content = "- Item\n\n continuation\n";
602 let fixed = fix(content);
603 assert_eq!(fixed, "- Item\n\n continuation\n");
604 }
605
606 #[test]
607 fn fix_ordered_indent() {
608 let content = "1. Item\n\n continuation\n";
609 let fixed = fix(content);
610 assert_eq!(fixed, "1. Item\n\n continuation\n");
611 }
612
613 #[test]
614 fn fix_mkdocs_indent() {
615 let content = "1. Item\n\n continuation\n";
616 let fixed = fix_mkdocs(content);
617 assert_eq!(fixed, "1. Item\n\n continuation\n");
618 }
619
620 #[test]
623 fn nested_list_items_not_flagged() {
624 let content = "- Parent\n\n - Child\n";
625 assert!(check(content).is_empty());
626 }
627
628 #[test]
629 fn nested_list_zero_indent_is_new_paragraph() {
630 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
632 assert!(check(content).is_empty());
633 }
634
635 #[test]
636 fn nested_list_partial_indent_flagged() {
637 let content = "- Parent\n - Child\n\n continuation of parent\n";
639 let warnings = check(content);
640 assert_eq!(warnings.len(), 1);
641 assert!(warnings[0].message.contains("2 spaces"));
642 }
643
644 #[test]
647 fn code_block_correctly_indented_no_warning() {
648 let content = "- Item\n\n ```\n code\n ```\n";
650 assert!(check(content).is_empty());
651 }
652
653 #[test]
654 fn code_fence_under_indented_warns() {
655 let content = "- Item\n\n ```\n code\n ```\n";
657 let warnings = check(content);
658 assert_eq!(warnings.len(), 2);
660 }
661
662 #[test]
663 fn code_fence_under_indented_ordered_mkdocs() {
664 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
667 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
669 assert_eq!(warnings.len(), 2); assert!(warnings[0].message.contains("4 spaces"));
671 assert!(warnings[0].message.contains("MkDocs"));
672 }
673
674 #[test]
675 fn code_fence_tilde_under_indented() {
676 let content = "- Item\n\n ~~~\n code\n ~~~\n";
677 let warnings = check(content);
678 assert_eq!(warnings.len(), 2); }
680
681 #[test]
684 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
685 let content = "- Item\n\n\ncontinuation\n";
687 assert!(check(content).is_empty());
688 }
689
690 #[test]
691 fn multiple_blank_lines_partial_indent_flags() {
692 let content = "- Item\n\n\n continuation\n";
693 let warnings = check(content);
694 assert_eq!(warnings.len(), 1);
695 }
696
697 #[test]
700 fn empty_item_no_warning() {
701 let content = "- \n- Second\n";
702 assert!(check(content).is_empty());
703 }
704
705 #[test]
708 fn multiple_items_mixed_indent() {
709 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
710 let warnings = check(content);
711 assert_eq!(warnings.len(), 1);
712 assert_eq!(warnings[0].line, 7);
713 }
714
715 #[test]
718 fn task_list_correct_indent() {
719 let content = "- [ ] Task\n\n continuation\n";
721 assert!(check(content).is_empty());
722 }
723
724 #[test]
727 fn frontmatter_not_flagged() {
728 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
729 assert!(check(content).is_empty());
730 }
731
732 #[test]
735 fn fix_multiple_items() {
736 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
737 let fixed = fix(content);
738 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
739 }
740
741 #[test]
742 fn fix_multiline_loose_continuation_all_lines() {
743 let content = "1. Item\n\n line one\n line two\n line three\n";
744 let fixed = fix(content);
745 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
746 }
747
748 #[test]
751 fn sibling_item_boundary_respected() {
752 let content = "- First\n- Second\n\n continuation\n";
754 assert!(check(content).is_empty());
755 }
756
757 #[test]
760 fn blockquote_list_correct_indent_no_warning() {
761 let content = "> - Item\n>\n> continuation\n";
764 assert!(check(content).is_empty());
765 }
766
767 #[test]
768 fn blockquote_list_under_indent_no_false_positive() {
769 let content = "> - Item\n>\n> continuation\n";
774 assert!(check(content).is_empty());
775 }
776
777 #[test]
780 fn deeply_nested_correct_indent() {
781 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
782 assert!(check(content).is_empty());
783 }
784
785 #[test]
786 fn deeply_nested_under_indent() {
787 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
790 let warnings = check(content);
791 assert_eq!(warnings.len(), 1);
792 assert!(warnings[0].message.contains("6 spaces"));
793 assert!(warnings[0].message.contains("found 5"));
794 }
795
796 #[test]
799 fn tab_indent_correct() {
800 let content = "- Item\n\n\tcontinuation\n";
803 assert!(check(content).is_empty());
804 }
805
806 #[test]
809 fn multiple_continuations_correct() {
810 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
811 assert!(check(content).is_empty());
812 }
813
814 #[test]
815 fn multiple_continuations_second_under_indent() {
816 let content = "- Item\n\n para 1\n\n continuation 2\n";
818 let warnings = check(content);
819 assert_eq!(warnings.len(), 1);
820 assert_eq!(warnings[0].line, 5);
821 }
822
823 #[test]
826 fn ordered_paren_marker_correct() {
827 let content = "1) Item\n\n continuation\n";
829 assert!(check(content).is_empty());
830 }
831
832 #[test]
833 fn ordered_paren_marker_under_indent() {
834 let content = "1) Item\n\n continuation\n";
835 let warnings = check(content);
836 assert_eq!(warnings.len(), 1);
837 assert!(warnings[0].message.contains("3 spaces"));
838 }
839
840 #[test]
843 fn star_marker_correct() {
844 let content = "* Item\n\n continuation\n";
845 assert!(check(content).is_empty());
846 }
847
848 #[test]
849 fn star_marker_under_indent() {
850 let content = "* Item\n\n continuation\n";
851 let warnings = check(content);
852 assert_eq!(warnings.len(), 1);
853 }
854
855 #[test]
856 fn plus_marker_correct() {
857 let content = "+ Item\n\n continuation\n";
858 assert!(check(content).is_empty());
859 }
860
861 #[test]
864 fn heading_after_list_no_warning() {
865 let content = "- Item\n\n# Heading\n";
866 assert!(check(content).is_empty());
867 }
868
869 #[test]
872 fn hr_after_list_no_warning() {
873 let content = "- Item\n\n---\n";
874 assert!(check(content).is_empty());
875 }
876
877 #[test]
880 fn reference_link_def_not_flagged() {
881 let content = "- Item\n\n [link]: https://example.com\n";
882 assert!(check(content).is_empty());
883 }
884
885 #[test]
888 fn footnote_def_not_flagged() {
889 let content = "- Item\n\n [^1]: footnote text\n";
890 assert!(check(content).is_empty());
891 }
892
893 #[test]
896 fn fix_deeply_nested() {
897 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
898 let fixed = fix(content);
899 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
900 }
901
902 #[test]
903 fn fix_mkdocs_unordered() {
904 let content = "- Item\n\n continuation\n";
906 let fixed = fix_mkdocs(content);
907 assert_eq!(fixed, "- Item\n\n continuation\n");
908 }
909
910 #[test]
911 fn fix_code_fence_indent() {
912 let content = "- Item\n\n ```\n code\n ```\n";
914 let fixed = fix(content);
915 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
916 }
917
918 #[test]
919 fn fix_mkdocs_code_fence_indent() {
920 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
922 let fixed = fix_mkdocs(content);
923 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
924 }
925
926 #[test]
929 fn empty_document_no_warning() {
930 assert!(check("").is_empty());
931 }
932
933 #[test]
934 fn whitespace_only_no_warning() {
935 assert!(check(" \n\n \n").is_empty());
936 }
937
938 #[test]
941 fn no_list_no_warning() {
942 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
943 assert!(check(content).is_empty());
944 }
945
946 #[test]
949 fn multiline_continuation_all_lines_flagged() {
950 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";
951 let warnings = check(content);
952 assert_eq!(warnings.len(), 3);
953 assert_eq!(warnings[0].line, 3);
954 assert_eq!(warnings[1].line, 4);
955 assert_eq!(warnings[2].line, 5);
956 }
957
958 #[test]
959 fn multiline_continuation_with_frontmatter_fix() {
960 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";
961 let fixed = fix(content);
962 assert_eq!(
963 fixed,
964 "---\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"
965 );
966 }
967
968 #[test]
969 fn multiline_continuation_correct_indent_no_warning() {
970 let content = "1. Item\n\n line one\n line two\n line three\n";
971 assert!(check(content).is_empty());
972 }
973
974 #[test]
975 fn multiline_continuation_mixed_indent() {
976 let content = "1. Item\n\n correct\n wrong\n correct\n";
977 let warnings = check(content);
978 assert_eq!(warnings.len(), 1);
979 assert_eq!(warnings[0].line, 4);
980 }
981
982 #[test]
983 fn multiline_continuation_unordered() {
984 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
985 let warnings = check(content);
986 assert_eq!(warnings.len(), 3);
987 let fixed = fix(content);
988 assert_eq!(
989 fixed,
990 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
991 );
992 }
993
994 #[test]
995 fn multiline_continuation_two_items_fix() {
996 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
997 let fixed = fix(content);
998 assert_eq!(
999 fixed,
1000 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1001 );
1002 }
1003
1004 #[test]
1005 fn multiline_continuation_separated_by_blank() {
1006 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1007 let warnings = check(content);
1008 assert_eq!(warnings.len(), 4);
1009 let fixed = fix(content);
1010 assert_eq!(
1011 fixed,
1012 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1013 );
1014 }
1015}