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(|&(ln, _, _)| ln - 1)
135 .unwrap_or(total_lines);
136
137 let mut saw_blank = false;
138 let mut nested_content_col: Option<usize> = None;
141
142 for line_num in (item_line + 1)..=range_end {
143 let Some(line_info) = ctx.line_info(line_num) else {
144 continue;
145 };
146
147 let trimmed = line_info.content(ctx.content).trim_start();
148
149 if Self::should_skip_line(line_info, trimmed) {
150 continue;
151 }
152
153 if line_info.is_blank {
154 saw_blank = true;
155 continue;
156 }
157
158 if let Some(ref li) = line_info.list_item {
160 if li.marker_column > marker_col {
161 nested_content_col = Some(li.content_column);
162 } else {
163 nested_content_col = None;
164 }
165 saw_blank = false;
166 continue;
167 }
168
169 if line_info.heading.is_some() {
171 break;
172 }
173
174 if line_info.is_horizontal_rule {
176 break;
177 }
178
179 if Self::is_block_level_construct(trimmed) {
182 continue;
183 }
184
185 let actual = line_info.visual_indent;
186
187 if let Some(ncc) = nested_content_col {
190 if actual >= ncc {
191 continue;
192 }
193 nested_content_col = None;
194 }
195
196 if !saw_blank {
198 if actual > required && !Self::starts_with_list_marker(trimmed) && flagged_lines.insert(line_num) {
199 let line_content = line_info.content(ctx.content);
200 let fix_start = line_info.byte_offset;
201 let fix_end = fix_start + line_info.indent;
202
203 warnings.push(LintWarning {
204 rule_name: Some("MD077".to_string()),
205 line: line_num,
206 column: 1,
207 end_line: line_num,
208 end_column: line_content.len() + 1,
209 message: format!("Continuation line over-indented (expected {required}, found {actual})",),
210 severity: Severity::Warning,
211 fix: Some(Fix {
212 range: fix_start..fix_end,
213 replacement: " ".repeat(required),
214 }),
215 });
216 }
217 continue;
218 }
219
220 if actual <= marker_col {
224 break;
225 }
226
227 if actual < required && flagged_lines.insert(line_num) {
228 let line_content = line_info.content(ctx.content);
229
230 let message = if strict_indent {
231 format!(
232 "Content inside list item needs {required} spaces of indentation \
233 for MkDocs compatibility (found {actual})",
234 )
235 } else {
236 format!(
237 "Content after blank line in list item needs {required} spaces of \
238 indentation to remain part of the list (found {actual})",
239 )
240 };
241
242 let fix_start = line_info.byte_offset;
244 let fix_end = fix_start + line_info.indent;
245
246 warnings.push(LintWarning {
247 rule_name: Some("MD077".to_string()),
248 line: line_num,
249 column: 1,
250 end_line: line_num,
251 end_column: line_content.len() + 1,
252 message,
253 severity: Severity::Warning,
254 fix: Some(Fix {
255 range: fix_start..fix_end,
256 replacement: " ".repeat(required),
257 }),
258 });
259 }
260
261 }
266 }
267
268 Ok(warnings)
269 }
270
271 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
272 let warnings = self.check(ctx)?;
273 let warnings =
274 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
275 if warnings.is_empty() {
276 return Ok(ctx.content.to_string());
277 }
278
279 let mut fixes: Vec<Fix> = warnings.into_iter().filter_map(|w| w.fix).collect();
281 fixes.sort_by_key(|f| std::cmp::Reverse(f.range.start));
282
283 let mut content = ctx.content.to_string();
284 for fix in fixes {
285 if fix.range.start <= content.len() && fix.range.end <= content.len() {
286 content.replace_range(fix.range, &fix.replacement);
287 }
288 }
289
290 Ok(content)
291 }
292
293 fn category(&self) -> RuleCategory {
294 RuleCategory::List
295 }
296
297 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
298 ctx.content.is_empty() || ctx.list_blocks.is_empty()
299 }
300
301 fn as_any(&self) -> &dyn std::any::Any {
302 self
303 }
304
305 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
306 where
307 Self: Sized,
308 {
309 Box::new(Self)
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use crate::config::MarkdownFlavor;
317
318 fn check(content: &str) -> Vec<LintWarning> {
319 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
320 let rule = MD077ListContinuationIndent;
321 rule.check(&ctx).unwrap()
322 }
323
324 fn check_mkdocs(content: &str) -> Vec<LintWarning> {
325 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
326 let rule = MD077ListContinuationIndent;
327 rule.check(&ctx).unwrap()
328 }
329
330 fn fix(content: &str) -> String {
331 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
332 let rule = MD077ListContinuationIndent;
333 rule.fix(&ctx).unwrap()
334 }
335
336 fn fix_mkdocs(content: &str) -> String {
337 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
338 let rule = MD077ListContinuationIndent;
339 rule.fix(&ctx).unwrap()
340 }
341
342 #[test]
345 fn tight_lazy_continuation_zero_indent_not_flagged() {
346 let content = "- Item\ncontinuation\n";
348 assert!(check(content).is_empty());
349 }
350
351 #[test]
352 fn tight_continuation_correct_indent_not_flagged() {
353 let content = "1. Item\n continuation\n";
355 assert!(check(content).is_empty());
356 }
357
358 #[test]
359 fn tight_continuation_over_indented_ordered() {
360 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
362 let warnings = check(content);
363 assert_eq!(warnings.len(), 1);
364 assert_eq!(warnings[0].line, 2);
365 assert!(warnings[0].message.contains("over-indented"));
366 }
367
368 #[test]
369 fn tight_continuation_over_indented_unordered() {
370 let content = "- Item\n over-indented\n";
372 let warnings = check(content);
373 assert_eq!(warnings.len(), 1);
374 assert_eq!(warnings[0].line, 2);
375 }
376
377 #[test]
378 fn tight_continuation_multiple_over_indented_lines() {
379 let content = "1. Item\n line one\n line two\n line three\n";
380 let warnings = check(content);
381 assert_eq!(warnings.len(), 3);
382 }
383
384 #[test]
385 fn tight_continuation_mixed_correct_and_over() {
386 let content = "1. Item\n correct\n over-indented\n correct again\n";
387 let warnings = check(content);
388 assert_eq!(warnings.len(), 1);
389 assert_eq!(warnings[0].line, 3);
390 }
391
392 #[test]
393 fn tight_continuation_nested_over_indented() {
394 let content = "- L1\n - L2\n over-indented continuation of L2\n";
396 let warnings = check(content);
397 assert_eq!(warnings.len(), 1);
398 assert_eq!(warnings[0].line, 3);
399 assert!(warnings[0].message.contains("expected 4"));
401 assert!(warnings[0].message.contains("found 5"));
402 }
403
404 #[test]
405 fn tight_continuation_nested_correct_indent_not_flagged() {
406 let content = "- L1\n - L2\n correctly indented continuation of L2\n";
409 assert!(check(content).is_empty());
410 }
411
412 #[test]
413 fn fix_tight_continuation_nested_over_indented() {
414 let content = "- L1\n - L2\n over-indented continuation of L2\n";
416 let fixed = fix(content);
417 assert_eq!(fixed, "- L1\n - L2\n over-indented continuation of L2\n");
418 }
419
420 #[test]
421 fn tight_continuation_under_indented_not_flagged() {
422 let content = "1. Item\n under-indented\n";
425 assert!(check(content).is_empty());
426 }
427
428 #[test]
429 fn tight_continuation_tab_over_indented() {
430 let content = "- Item\n\tover-indented\n";
432 let warnings = check(content);
433 assert_eq!(warnings.len(), 1);
434 }
435
436 #[test]
437 fn fix_tight_continuation_over_indented_ordered() {
438 let content = "1. This is a list item with multiple lines.\n The second line is over-indented.\n";
439 let fixed = fix(content);
440 assert_eq!(
441 fixed,
442 "1. This is a list item with multiple lines.\n The second line is over-indented.\n"
443 );
444 }
445
446 #[test]
447 fn fix_tight_continuation_over_indented_unordered() {
448 let content = "- Item\n over-indented\n";
449 let fixed = fix(content);
450 assert_eq!(fixed, "- Item\n over-indented\n");
451 }
452
453 #[test]
454 fn fix_tight_continuation_multiple_lines() {
455 let content = "1. Item\n line one\n line two\n";
456 let fixed = fix(content);
457 assert_eq!(fixed, "1. Item\n line one\n line two\n");
458 }
459
460 #[test]
461 fn tight_continuation_mkdocs_4space_ordered_not_flagged() {
462 let content = "1. Item\n continuation\n";
465 assert!(check_mkdocs(content).is_empty());
466 }
467
468 #[test]
469 fn tight_continuation_mkdocs_5space_ordered_flagged() {
470 let content = "1. Item\n over-indented\n";
472 let warnings = check_mkdocs(content);
473 assert_eq!(warnings.len(), 1);
474 assert!(warnings[0].message.contains("expected 4"));
475 assert!(warnings[0].message.contains("found 5"));
476 }
477
478 #[test]
479 fn fix_tight_continuation_mkdocs_over_indented() {
480 let content = "1. Item\n over-indented\n";
481 let fixed = fix_mkdocs(content);
482 assert_eq!(fixed, "1. Item\n over-indented\n");
483 }
484
485 #[test]
486 fn tight_continuation_deeply_indented_list_markers_not_flagged() {
487 let content = "* Level 0\n * Level 1\n * Level 2\n";
490 assert!(check(content).is_empty());
491 }
492
493 #[test]
494 fn tight_continuation_ordered_marker_not_flagged() {
495 let content = "- Parent\n 1. Child item\n";
497 assert!(check(content).is_empty());
498 }
499
500 #[test]
503 fn unordered_correct_indent_no_warning() {
504 let content = "- Item\n\n continuation\n";
505 assert!(check(content).is_empty());
506 }
507
508 #[test]
509 fn unordered_partial_indent_warns() {
510 let content = "- Item\n\n continuation\n";
513 let warnings = check(content);
514 assert_eq!(warnings.len(), 1);
515 assert_eq!(warnings[0].line, 3);
516 assert!(warnings[0].message.contains("2 spaces"));
517 assert!(warnings[0].message.contains("found 1"));
518 }
519
520 #[test]
521 fn unordered_zero_indent_is_new_paragraph() {
522 let content = "- Item\n\ncontinuation\n";
525 assert!(check(content).is_empty());
526 }
527
528 #[test]
531 fn ordered_3space_correct_commonmark() {
532 let content = "1. Item\n\n continuation\n";
534 assert!(check(content).is_empty());
535 }
536
537 #[test]
538 fn ordered_2space_under_indent_commonmark() {
539 let content = "1. Item\n\n continuation\n";
540 let warnings = check(content);
541 assert_eq!(warnings.len(), 1);
542 assert!(warnings[0].message.contains("3 spaces"));
543 assert!(warnings[0].message.contains("found 2"));
544 }
545
546 #[test]
549 fn multi_digit_marker_correct() {
550 let content = "10. Item\n\n continuation\n";
552 assert!(check(content).is_empty());
553 }
554
555 #[test]
556 fn multi_digit_marker_under_indent() {
557 let content = "10. Item\n\n continuation\n";
558 let warnings = check(content);
559 assert_eq!(warnings.len(), 1);
560 assert!(warnings[0].message.contains("4 spaces"));
561 }
562
563 #[test]
566 fn mkdocs_3space_ordered_warns() {
567 let content = "1. Item\n\n continuation\n";
569 let warnings = check_mkdocs(content);
570 assert_eq!(warnings.len(), 1);
571 assert!(warnings[0].message.contains("4 spaces"));
572 assert!(warnings[0].message.contains("MkDocs"));
573 }
574
575 #[test]
576 fn mkdocs_4space_ordered_no_warning() {
577 let content = "1. Item\n\n continuation\n";
578 assert!(check_mkdocs(content).is_empty());
579 }
580
581 #[test]
582 fn mkdocs_unordered_2space_ok() {
583 let content = "- Item\n\n continuation\n";
585 assert!(check_mkdocs(content).is_empty());
586 }
587
588 #[test]
589 fn mkdocs_unordered_2space_warns() {
590 let content = "- Item\n\n continuation\n";
592 let warnings = check_mkdocs(content);
593 assert_eq!(warnings.len(), 1);
594 assert!(warnings[0].message.contains("4 spaces"));
595 }
596
597 #[test]
600 fn fix_unordered_indent() {
601 let content = "- Item\n\n continuation\n";
603 let fixed = fix(content);
604 assert_eq!(fixed, "- Item\n\n continuation\n");
605 }
606
607 #[test]
608 fn fix_ordered_indent() {
609 let content = "1. Item\n\n continuation\n";
610 let fixed = fix(content);
611 assert_eq!(fixed, "1. Item\n\n continuation\n");
612 }
613
614 #[test]
615 fn fix_mkdocs_indent() {
616 let content = "1. Item\n\n continuation\n";
617 let fixed = fix_mkdocs(content);
618 assert_eq!(fixed, "1. Item\n\n continuation\n");
619 }
620
621 #[test]
624 fn nested_list_items_not_flagged() {
625 let content = "- Parent\n\n - Child\n";
626 assert!(check(content).is_empty());
627 }
628
629 #[test]
630 fn nested_list_zero_indent_is_new_paragraph() {
631 let content = "- Parent\n - Child\n\ncontinuation of parent\n";
633 assert!(check(content).is_empty());
634 }
635
636 #[test]
637 fn nested_list_partial_indent_flagged() {
638 let content = "- Parent\n - Child\n\n continuation of parent\n";
640 let warnings = check(content);
641 assert_eq!(warnings.len(), 1);
642 assert!(warnings[0].message.contains("2 spaces"));
643 }
644
645 #[test]
648 fn code_block_correctly_indented_no_warning() {
649 let content = "- Item\n\n ```\n code\n ```\n";
651 assert!(check(content).is_empty());
652 }
653
654 #[test]
655 fn code_fence_under_indented_warns() {
656 let content = "- Item\n\n ```\n code\n ```\n";
658 let warnings = check(content);
659 assert_eq!(warnings.len(), 2);
661 }
662
663 #[test]
664 fn code_fence_under_indented_ordered_mkdocs() {
665 let content = "1. Item\n\n ```toml\n key = \"value\"\n ```\n";
668 assert!(check(content).is_empty()); let warnings = check_mkdocs(content);
670 assert_eq!(warnings.len(), 2); assert!(warnings[0].message.contains("4 spaces"));
672 assert!(warnings[0].message.contains("MkDocs"));
673 }
674
675 #[test]
676 fn code_fence_tilde_under_indented() {
677 let content = "- Item\n\n ~~~\n code\n ~~~\n";
678 let warnings = check(content);
679 assert_eq!(warnings.len(), 2); }
681
682 #[test]
685 fn multiple_blank_lines_zero_indent_is_new_paragraph() {
686 let content = "- Item\n\n\ncontinuation\n";
688 assert!(check(content).is_empty());
689 }
690
691 #[test]
692 fn multiple_blank_lines_partial_indent_flags() {
693 let content = "- Item\n\n\n continuation\n";
694 let warnings = check(content);
695 assert_eq!(warnings.len(), 1);
696 }
697
698 #[test]
701 fn empty_item_no_warning() {
702 let content = "- \n- Second\n";
703 assert!(check(content).is_empty());
704 }
705
706 #[test]
709 fn multiple_items_mixed_indent() {
710 let content = "1. First\n\n correct continuation\n\n2. Second\n\n wrong continuation\n";
711 let warnings = check(content);
712 assert_eq!(warnings.len(), 1);
713 assert_eq!(warnings[0].line, 7);
714 }
715
716 #[test]
719 fn task_list_correct_indent() {
720 let content = "- [ ] Task\n\n continuation\n";
722 assert!(check(content).is_empty());
723 }
724
725 #[test]
728 fn frontmatter_not_flagged() {
729 let content = "---\ntitle: test\n---\n\n- Item\n\n continuation\n";
730 assert!(check(content).is_empty());
731 }
732
733 #[test]
736 fn fix_multiple_items() {
737 let content = "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n";
738 let fixed = fix(content);
739 assert_eq!(fixed, "1. First\n\n wrong1\n\n2. Second\n\n wrong2\n");
740 }
741
742 #[test]
743 fn fix_multiline_loose_continuation_all_lines() {
744 let content = "1. Item\n\n line one\n line two\n line three\n";
745 let fixed = fix(content);
746 assert_eq!(fixed, "1. Item\n\n line one\n line two\n line three\n");
747 }
748
749 #[test]
752 fn sibling_item_boundary_respected() {
753 let content = "- First\n- Second\n\n continuation\n";
755 assert!(check(content).is_empty());
756 }
757
758 #[test]
761 fn blockquote_list_correct_indent_no_warning() {
762 let content = "> - Item\n>\n> continuation\n";
765 assert!(check(content).is_empty());
766 }
767
768 #[test]
769 fn blockquote_list_under_indent_no_false_positive() {
770 let content = "> - Item\n>\n> continuation\n";
775 assert!(check(content).is_empty());
776 }
777
778 #[test]
781 fn deeply_nested_correct_indent() {
782 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
783 assert!(check(content).is_empty());
784 }
785
786 #[test]
787 fn deeply_nested_under_indent() {
788 let content = "- L1\n - L2\n - L3\n\n continuation of L3\n";
791 let warnings = check(content);
792 assert_eq!(warnings.len(), 1);
793 assert!(warnings[0].message.contains("6 spaces"));
794 assert!(warnings[0].message.contains("found 5"));
795 }
796
797 #[test]
800 fn tab_indent_correct() {
801 let content = "- Item\n\n\tcontinuation\n";
804 assert!(check(content).is_empty());
805 }
806
807 #[test]
810 fn multiple_continuations_correct() {
811 let content = "- Item\n\n para 1\n\n para 2\n\n para 3\n";
812 assert!(check(content).is_empty());
813 }
814
815 #[test]
816 fn multiple_continuations_second_under_indent() {
817 let content = "- Item\n\n para 1\n\n continuation 2\n";
819 let warnings = check(content);
820 assert_eq!(warnings.len(), 1);
821 assert_eq!(warnings[0].line, 5);
822 }
823
824 #[test]
827 fn ordered_paren_marker_correct() {
828 let content = "1) Item\n\n continuation\n";
830 assert!(check(content).is_empty());
831 }
832
833 #[test]
834 fn ordered_paren_marker_under_indent() {
835 let content = "1) Item\n\n continuation\n";
836 let warnings = check(content);
837 assert_eq!(warnings.len(), 1);
838 assert!(warnings[0].message.contains("3 spaces"));
839 }
840
841 #[test]
844 fn star_marker_correct() {
845 let content = "* Item\n\n continuation\n";
846 assert!(check(content).is_empty());
847 }
848
849 #[test]
850 fn star_marker_under_indent() {
851 let content = "* Item\n\n continuation\n";
852 let warnings = check(content);
853 assert_eq!(warnings.len(), 1);
854 }
855
856 #[test]
857 fn plus_marker_correct() {
858 let content = "+ Item\n\n continuation\n";
859 assert!(check(content).is_empty());
860 }
861
862 #[test]
865 fn heading_after_list_no_warning() {
866 let content = "- Item\n\n# Heading\n";
867 assert!(check(content).is_empty());
868 }
869
870 #[test]
873 fn hr_after_list_no_warning() {
874 let content = "- Item\n\n---\n";
875 assert!(check(content).is_empty());
876 }
877
878 #[test]
881 fn reference_link_def_not_flagged() {
882 let content = "- Item\n\n [link]: https://example.com\n";
883 assert!(check(content).is_empty());
884 }
885
886 #[test]
889 fn footnote_def_not_flagged() {
890 let content = "- Item\n\n [^1]: footnote text\n";
891 assert!(check(content).is_empty());
892 }
893
894 #[test]
897 fn fix_deeply_nested() {
898 let content = "- L1\n - L2\n - L3\n\n under-indented\n";
899 let fixed = fix(content);
900 assert_eq!(fixed, "- L1\n - L2\n - L3\n\n under-indented\n");
901 }
902
903 #[test]
904 fn fix_mkdocs_unordered() {
905 let content = "- Item\n\n continuation\n";
907 let fixed = fix_mkdocs(content);
908 assert_eq!(fixed, "- Item\n\n continuation\n");
909 }
910
911 #[test]
912 fn fix_code_fence_indent() {
913 let content = "- Item\n\n ```\n code\n ```\n";
915 let fixed = fix(content);
916 assert_eq!(fixed, "- Item\n\n ```\n code\n ```\n");
917 }
918
919 #[test]
920 fn fix_mkdocs_code_fence_indent() {
921 let content = "1. Item\n\n ```toml\n key = \"val\"\n ```\n";
923 let fixed = fix_mkdocs(content);
924 assert_eq!(fixed, "1. Item\n\n ```toml\n key = \"val\"\n ```\n");
925 }
926
927 #[test]
930 fn empty_document_no_warning() {
931 assert!(check("").is_empty());
932 }
933
934 #[test]
935 fn whitespace_only_no_warning() {
936 assert!(check(" \n\n \n").is_empty());
937 }
938
939 #[test]
942 fn no_list_no_warning() {
943 let content = "# Heading\n\nSome paragraph.\n\nAnother paragraph.\n";
944 assert!(check(content).is_empty());
945 }
946
947 #[test]
950 fn multiline_continuation_all_lines_flagged() {
951 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";
952 let warnings = check(content);
953 assert_eq!(warnings.len(), 3);
954 assert_eq!(warnings[0].line, 3);
955 assert_eq!(warnings[1].line, 4);
956 assert_eq!(warnings[2].line, 5);
957 }
958
959 #[test]
960 fn multiline_continuation_with_frontmatter_fix() {
961 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";
962 let fixed = fix(content);
963 assert_eq!(
964 fixed,
965 "---\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"
966 );
967 }
968
969 #[test]
970 fn multiline_continuation_correct_indent_no_warning() {
971 let content = "1. Item\n\n line one\n line two\n line three\n";
972 assert!(check(content).is_empty());
973 }
974
975 #[test]
976 fn multiline_continuation_mixed_indent() {
977 let content = "1. Item\n\n correct\n wrong\n correct\n";
978 let warnings = check(content);
979 assert_eq!(warnings.len(), 1);
980 assert_eq!(warnings[0].line, 4);
981 }
982
983 #[test]
984 fn multiline_continuation_unordered() {
985 let content = "- Item\n\n continuation 1\n continuation 2\n continuation 3\n";
986 let warnings = check(content);
987 assert_eq!(warnings.len(), 3);
988 let fixed = fix(content);
989 assert_eq!(
990 fixed,
991 "- Item\n\n continuation 1\n continuation 2\n continuation 3\n"
992 );
993 }
994
995 #[test]
996 fn multiline_continuation_two_items_fix() {
997 let content = "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n";
998 let fixed = fix(content);
999 assert_eq!(
1000 fixed,
1001 "1. First\n\n cont a\n cont b\n\n2. Second\n\n cont c\n cont d\n"
1002 );
1003 }
1004
1005 #[test]
1006 fn multiline_continuation_separated_by_blank() {
1007 let content = "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n";
1008 let warnings = check(content);
1009 assert_eq!(warnings.len(), 4);
1010 let fixed = fix(content);
1011 assert_eq!(
1012 fixed,
1013 "1. Item\n\n para1 line1\n para1 line2\n\n para2 line1\n para2 line2\n"
1014 );
1015 }
1016}