#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)] pub struct ListItem {
pub marker: String,
pub content: String,
pub level: usize,
pub is_task: bool,
pub checked: Option<bool>,
pub line_number: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)] pub struct List {
pub items: Vec<ListItem>,
pub start_line: usize,
pub end_line: usize,
pub is_ordered: bool,
}
#[must_use]
#[allow(dead_code)] pub fn detect_lists(content: &str) -> Vec<List> {
let mut lists = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let code_line_ranges = get_code_block_line_ranges(content);
let mut i = 0;
while i < lines.len() {
let line = lines[i];
if is_in_code_region(i, &code_line_ranges) {
i += 1;
continue;
}
if let Some(item) = parse_list_item(line, i) {
let mut current_list_items = vec![item];
let start_line = i;
i += 1;
while i < lines.len() {
let next_line = lines[i];
if is_in_code_region(i, &code_line_ranges) {
break;
}
if next_line.trim().is_empty() {
if i + 1 < lines.len() {
if let Some(next_item) = parse_list_item(lines[i + 1], i + 1) {
if is_same_list(¤t_list_items, &next_item) {
i += 1; continue;
}
}
}
break;
}
if let Some(next_item) = parse_list_item(next_line, i) {
if is_same_list(¤t_list_items, &next_item)
|| is_nested_list(¤t_list_items, &next_item)
{
current_list_items.push(next_item);
i += 1;
continue;
}
}
if is_continuation_line(next_line, ¤t_list_items) {
i += 1;
continue;
}
break;
}
if !current_list_items.is_empty() {
let is_ordered = current_list_items[0].marker.parse::<i32>().is_ok();
lists.push(List {
items: current_list_items,
start_line,
end_line: i - 1,
is_ordered,
});
}
} else {
i += 1;
}
}
lists
}
fn get_code_block_line_ranges(content: &str) -> Vec<(usize, usize)> {
let mut ranges = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let mut in_code_block = false;
let mut block_start = 0;
for (i, &line) in lines.iter().enumerate() {
let trimmed = line.trim();
if is_fence_line(trimmed) {
if in_code_block {
ranges.push((block_start, i));
in_code_block = false;
} else {
block_start = i;
in_code_block = true;
}
}
}
if in_code_block {
ranges.push((block_start, lines.len() - 1));
}
ranges
}
fn is_fence_line(line: &str) -> bool {
line.starts_with("```") || line.starts_with("~~~")
}
fn parse_list_item(line: &str, line_number: usize) -> Option<ListItem> {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("- ") {
return parse_task_or_item("-", rest, line_number);
}
if let Some(rest) = trimmed.strip_prefix("* ") {
return Some(ListItem {
marker: "*".to_string(),
content: rest.to_string(),
level: 0, is_task: false,
checked: None,
line_number,
});
}
if let Some(rest) = trimmed.strip_prefix("+ ") {
return Some(ListItem {
marker: "+".to_string(),
content: rest.to_string(),
level: 0,
is_task: false,
checked: None,
line_number,
});
}
let chars: Vec<char> = trimmed.chars().collect();
if !chars.is_empty() && chars[0].is_ascii_digit() {
let mut num_end = 0;
while num_end < chars.len() && chars[num_end].is_ascii_digit() {
num_end += 1;
}
if num_end < chars.len() && (chars[num_end] == '.' || chars[num_end] == ')') {
let number = &trimmed[0..num_end];
let rest = trimmed[num_end + 1..].trim_start();
return Some(ListItem {
marker: number.to_string(),
content: rest.to_string(),
level: 0,
is_task: false,
checked: None,
line_number,
});
}
}
None
}
fn parse_task_or_item(marker: &str, content: &str, line_number: usize) -> Option<ListItem> {
let trimmed = content.trim_start();
if let Some(rest) = trimmed.strip_prefix("[ ] ") {
return Some(ListItem {
marker: marker.to_string(),
content: rest.to_string(),
level: 0,
is_task: true,
checked: Some(false),
line_number,
});
}
if let Some(rest) = trimmed.strip_prefix("[x] ") {
return Some(ListItem {
marker: marker.to_string(),
content: rest.to_string(),
level: 0,
is_task: true,
checked: Some(true),
line_number,
});
}
if let Some(rest) = trimmed.strip_prefix("[X] ") {
return Some(ListItem {
marker: marker.to_string(),
content: rest.to_string(),
level: 0,
is_task: true,
checked: Some(true),
line_number,
});
}
Some(ListItem {
marker: marker.to_string(),
content: content.to_string(),
level: 0,
is_task: false,
checked: None,
line_number,
})
}
#[allow(dead_code)] fn is_same_list(existing_items: &[ListItem], new_item: &ListItem) -> bool {
if existing_items.is_empty() {
return true;
}
let first = &existing_items[0];
if ["-", "*", "+"].contains(&first.marker.as_str())
&& ["-", "*", "+"].contains(&new_item.marker.as_str())
{
return true;
}
if first.marker.parse::<i32>().is_ok() && new_item.marker.parse::<i32>().is_ok() {
return true;
}
false
}
#[allow(dead_code)] fn is_nested_list(existing_items: &[ListItem], new_item: &ListItem) -> bool {
if existing_items.is_empty() {
return false;
}
new_item.line_number > existing_items[0].line_number
}
#[allow(dead_code)] fn is_continuation_line(line: &str, current_items: &[ListItem]) -> bool {
if current_items.is_empty() {
return false;
}
if line.trim().is_empty() {
return false;
}
let leading_spaces = line.len() - line.trim_start().len();
leading_spaces >= 2 && parse_list_item(line, 0).is_none()
}
fn is_in_code_region(line_num: usize, regions: &[(usize, usize)]) -> bool {
for (start, end) in regions {
if line_num >= *start && line_num <= *end {
return true;
}
}
false
}
fn required_indent_for_level(stack: &[(usize, String)], level: usize) -> String {
let mut total = 0;
for (_, marker) in stack.iter().take(level) {
if marker.parse::<i32>().is_ok() {
total += marker.len() + 2;
} else {
total += 2;
}
}
" ".repeat(total)
}
#[must_use]
#[allow(dead_code)] pub fn normalize_list_indentation(content: &str) -> String {
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() {
return String::new();
}
let code_ranges = get_code_block_line_ranges(content);
let mut result = Vec::new();
let mut list_stack: Vec<(usize, String)> = Vec::new();
for (i, &line) in lines.iter().enumerate() {
if is_in_code_region(i, &code_ranges) {
result.push(line.to_string());
continue;
}
if let Some(item) = parse_list_item(line, i) {
let current_indent = line.len() - line.trim_start().len();
let level = if list_stack.is_empty() {
list_stack.push((current_indent, item.marker.clone()));
0
} else {
let mut level = list_stack.len();
for (idx, (indent, _)) in list_stack.iter().enumerate() {
if current_indent <= *indent {
level = idx;
break;
}
}
list_stack.truncate(level);
if level == list_stack.len() {
list_stack.push((current_indent, item.marker.clone()));
}
level
};
let normalized_indent = required_indent_for_level(&list_stack, level);
let reconstructed = format!("{}{} {}", normalized_indent, item.marker, item.content);
result.push(reconstructed);
} else {
result.push(line.to_string());
}
}
result.join("\n")
}
#[must_use]
#[allow(dead_code)] pub fn normalize_bullet_styles(content: &str, target_bullet: char) -> String {
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() {
return String::new();
}
let target = match target_bullet {
'-' | '*' | '+' => target_bullet,
_ => '-', };
let code_ranges = get_code_block_line_ranges(content);
let mut result = Vec::new();
for (i, &line) in lines.iter().enumerate() {
if is_in_code_region(i, &code_ranges) {
result.push(line.to_string());
continue;
}
if let Some(item) = parse_list_item(line, i) {
if ["-", "*", "+"].contains(&item.marker.as_str()) {
let indent = line.len() - line.trim_start().len();
let indent_str = " ".repeat(indent);
let reconstructed = if item.is_task {
format!(
"{}{} [{}] {}",
indent_str,
target,
if item.checked.unwrap_or(false) {
"x"
} else {
" "
},
item.content
)
} else {
format!("{}{} {}", indent_str, target, item.content)
};
result.push(reconstructed);
} else {
result.push(line.to_string());
}
} else {
result.push(line.to_string());
}
}
result.join("\n")
}
#[must_use]
pub fn normalize_lists(content: &str) -> String {
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() {
return String::new();
}
let code_ranges = get_code_block_line_ranges(content);
let mut result = Vec::new();
let mut list_stack: Vec<(usize, String)> = Vec::new();
let target_bullet = '-';
for (i, &line) in lines.iter().enumerate() {
if is_in_code_region(i, &code_ranges) {
result.push(line.to_string());
list_stack.clear();
continue;
}
if let Some(item) = parse_list_item(line, i) {
let current_indent = line.len() - line.trim_start().len();
let level = if list_stack.is_empty() {
list_stack.push((current_indent, item.marker.clone()));
0
} else {
let mut level = list_stack.len();
for (idx, (indent, _)) in list_stack.iter().enumerate() {
if current_indent <= *indent {
level = idx;
break;
}
}
list_stack.truncate(level);
if level == list_stack.len() {
list_stack.push((current_indent, item.marker.clone()));
}
level
};
let normalized_indent = required_indent_for_level(&list_stack, level);
let reconstructed = if item.is_task && ["-", "*", "+"].contains(&item.marker.as_str()) {
format!(
"{}{} [{}] {}",
normalized_indent,
target_bullet,
if item.checked.unwrap_or(false) {
"x"
} else {
" "
},
item.content
)
} else if ["-", "*", "+"].contains(&item.marker.as_str()) {
format!("{}{} {}", normalized_indent, target_bullet, item.content)
} else {
format!("{}{}. {}", normalized_indent, item.marker, item.content)
};
result.push(reconstructed);
} else {
result.push(line.to_string());
}
}
result.join("\n")
}
#[must_use]
#[allow(dead_code)] pub fn normalize_loose_lists(content: &str) -> String {
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() {
return String::new();
}
let code_line_ranges = get_code_block_line_ranges(content);
let mut result = Vec::new();
for (i, &line) in lines.iter().enumerate() {
if is_in_code_region(i, &code_line_ranges) {
result.push(line.to_string());
continue;
}
let is_list_item = parse_list_item(line, i).is_some();
let prev_line = if i > 0 { lines.get(i - 1) } else { None };
let prev_trimmed = prev_line.map_or("", |l| l.trim());
let needs_blank_line = if is_list_item {
let prev_was_header = prev_trimmed.starts_with('#');
let prev_was_list_item = prev_line.is_some_and(|l| parse_list_item(l, i - 1).is_some());
let prev_was_paragraph =
!prev_trimmed.is_empty() && !prev_was_header && !prev_was_list_item;
(prev_was_header || prev_was_paragraph)
&& !result.is_empty()
&& result.last().is_none_or(|s: &String| !s.trim().is_empty())
} else {
false
};
if needs_blank_line {
result.push(String::new());
}
result.push(line.to_string());
}
result.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_simple_bullet_list() {
let content = "- Item 1\n- Item 2\n- Item 3";
let lists = detect_lists(content);
assert_eq!(lists.len(), 1);
assert_eq!(lists[0].items.len(), 3);
assert_eq!(lists[0].items[0].content, "Item 1");
assert_eq!(lists[0].items[1].content, "Item 2");
assert_eq!(lists[0].items[2].content, "Item 3");
}
#[test]
fn detect_list_with_mixed_bullets() {
let content = "- Item 1\n* Item 2\n+ Item 3";
let lists = detect_lists(content);
assert_eq!(lists.len(), 1);
assert_eq!(lists[0].items.len(), 3);
}
#[test]
fn detect_ordered_list() {
let content = "1. First item\n2. Second item\n3. Third item";
let lists = detect_lists(content);
assert_eq!(lists.len(), 1);
assert_eq!(lists[0].items.len(), 3);
assert!(lists[0].is_ordered);
}
#[test]
fn detect_task_list() {
let content = "- [ ] Buy milk\n- [x] Done item\n- [X] Also done";
let lists = detect_lists(content);
assert_eq!(lists.len(), 1);
assert_eq!(lists[0].items.len(), 3);
assert!(lists[0].items[0].is_task);
assert!(!lists[0].items[0].checked.unwrap());
assert!(lists[0].items[1].is_task);
assert!(lists[0].items[1].checked.unwrap());
}
#[test]
fn ignore_lists_in_code_blocks() {
let content =
"```markdown\n- Item in code block\n- Another item\n```\n\n- Real item outside";
let lists = detect_lists(content);
assert_eq!(lists.len(), 1);
assert_eq!(lists[0].items.len(), 1);
assert_eq!(lists[0].items[0].content, "Real item outside");
}
#[test]
fn detect_multiple_lists() {
let content = "- First list item 1\n- First list item 2\n\nSome text\n\n* Second list item 1\n* Second list item 2";
let lists = detect_lists(content);
assert_eq!(lists.len(), 2);
assert_eq!(lists[0].items.len(), 2);
assert_eq!(lists[1].items.len(), 2);
}
#[test]
fn no_lists_in_plain_text() {
let content = "This is just a paragraph.\nNo lists here.\nJust text.";
let lists = detect_lists(content);
assert_eq!(lists.len(), 0);
}
#[test]
fn normalize_indentation_to_two_spaces() {
let content = "- Item 1\n - Nested item\n- Item 2";
let normalized = normalize_list_indentation(content);
assert!(normalized.contains("- Item 1"));
assert!(normalized.contains(" - Nested item")); assert!(!normalized.contains(" - Nested")); }
#[test]
fn normalize_deeply_nested_list() {
let content = "- Level 1\n - Level 2\n - Level 3\n- Back to 1";
let normalized = normalize_list_indentation(content);
assert!(normalized.contains("- Level 1"));
assert!(normalized.contains(" - Level 2")); assert!(normalized.contains(" - Level 3")); }
#[test]
fn preserve_content_when_normalizing() {
let content = "- First item with text\n - Second item with more text";
let normalized = normalize_list_indentation(content);
assert!(normalized.contains("First item with text"));
assert!(normalized.contains("Second item with more text"));
}
#[test]
fn no_change_to_already_normalized() {
let content = "- Item 1\n - Nested\n - Another nested\n- Item 2";
let normalized = normalize_list_indentation(content);
assert_eq!(normalized, content);
}
#[test]
fn normalize_mixed_indentation_styles() {
let content = "- Item 1\n - Two space\n - Four space (should be 4)\n- Item 2";
let normalized = normalize_list_indentation(content);
assert!(normalized.contains("- Item 1"));
assert!(normalized.contains(" - Two space"));
assert!(normalized.contains(" - Four space")); }
#[test]
fn normalize_bullet_styles_to_dash() {
let content = "- Item 1\n* Item 2\n+ Item 3";
let normalized = normalize_bullet_styles(content, '-');
assert!(normalized.contains("- Item 1"));
assert!(normalized.contains("- Item 2"));
assert!(normalized.contains("- Item 3"));
assert!(!normalized.contains("* Item"));
assert!(!normalized.contains("+ Item"));
}
#[test]
fn normalize_bullet_styles_to_asterisk() {
let content = "- Item 1\n* Item 2\n+ Item 3";
let normalized = normalize_bullet_styles(content, '*');
assert!(normalized.contains("* Item 1"));
assert!(normalized.contains("* Item 2"));
assert!(normalized.contains("* Item 3"));
}
#[test]
fn bullet_normalization_preserves_indentation() {
let content = "- Item 1\n * Nested\n + Deep";
let normalized = normalize_bullet_styles(content, '-');
assert!(normalized.contains("- Item 1"));
assert!(normalized.contains(" - Nested"));
assert!(normalized.contains(" - Deep"));
}
#[test]
fn bullet_normalization_preserves_task_lists() {
let content = "- [ ] Task\n* [x] Done\n+ [ ] Another";
let normalized = normalize_bullet_styles(content, '-');
assert!(normalized.contains("- [ ] Task"));
assert!(normalized.contains("- [x] Done"));
assert!(normalized.contains("- [ ] Another"));
}
#[test]
fn bullet_normalization_preserves_ordered_lists() {
let content = "1. First\n2. Second\n- Unordered";
let normalized = normalize_bullet_styles(content, '-');
assert!(normalized.contains("1. First"));
assert!(normalized.contains("2. Second"));
assert!(normalized.contains("- Unordered"));
}
#[test]
fn normalize_complex_nested_list() {
let content = "- Level 1\n * Level 2\n + Level 3\n- Back to 1";
let normalized = normalize_lists(content);
assert!(normalized.contains("- Level 1"));
assert!(normalized.contains(" - Level 2"));
assert!(normalized.contains(" - Level 3"));
assert!(normalized.contains("- Back to 1"));
}
#[test]
fn normalize_deeply_nested_structure() {
let content = "- A\n - B\n - C\n - D\n- E";
let normalized = normalize_lists(content);
assert!(normalized.contains("- A"));
assert!(normalized.contains(" - B"));
assert!(normalized.contains(" - C"));
assert!(normalized.contains(" - D"));
assert!(normalized.contains("- E"));
}
#[test]
fn normalize_nested_with_inconsistent_indentation() {
let content = "- Item 1\n - Nested with 4\n - Deeper\n- Item 2";
let normalized = normalize_lists(content);
assert!(normalized.contains("- Item 1"));
assert!(normalized.contains(" - Nested with 4")); assert!(normalized.contains(" - Deeper")); assert!(normalized.contains("- Item 2"));
}
#[test]
fn separate_adjacent_lists() {
let content =
"- First list A\n- First list B\n\nSome text\n\n* Second list A\n* Second list B";
let normalized = normalize_lists(content);
assert!(normalized.contains("- First list A"));
assert!(normalized.contains("- First list B"));
assert!(normalized.contains("- Second list A")); assert!(normalized.contains("- Second list B"));
}
#[test]
fn preserve_task_list_checkboxes() {
let content = "- [ ] Unchecked task\n- [x] Checked task\n- [X] Also checked";
let normalized = normalize_lists(content);
assert!(normalized.contains("- [ ] Unchecked task"));
assert!(normalized.contains("- [x] Checked task"));
assert!(normalized.contains("- [x] Also checked")); }
#[test]
fn normalize_mixed_task_and_regular() {
let content = "- [ ] Buy milk\n- Regular item\n- [x] Done item";
let normalized = normalize_lists(content);
assert!(normalized.contains("- [ ] Buy milk"));
assert!(normalized.contains("- Regular item"));
assert!(normalized.contains("- [x] Done item"));
}
#[test]
fn nested_task_lists() {
let content =
"- [ ] Parent task\n - [ ] Subtask 1\n - [x] Subtask 2\n- [ ] Another parent";
let normalized = normalize_lists(content);
assert!(normalized.contains("- [ ] Parent task"));
assert!(normalized.contains(" - [ ] Subtask 1"));
assert!(normalized.contains(" - [x] Subtask 2"));
assert!(normalized.contains("- [ ] Another parent"));
}
#[test]
fn task_list_with_bullet_normalization() {
let content = "- [ ] Task 1\n* [ ] Task 2\n+ [x] Done";
let normalized = normalize_lists(content);
assert!(normalized.contains("- [ ] Task 1"));
assert!(normalized.contains("- [ ] Task 2")); assert!(normalized.contains("- [x] Done")); }
#[test]
fn task_lists_in_code_blocks_preserved() {
let content =
"```markdown\n- [ ] In code block\n- [x] Also in block\n```\n\n- [ ] Real task outside";
let normalized = normalize_lists(content);
assert!(normalized.contains("- [ ] In code block")); assert!(normalized.contains("- [ ] Real task outside"));
}
#[test]
fn add_blank_line_between_header_and_list() {
let content = "# My Title\n- Item 1\n- Item 2";
let normalized = normalize_loose_lists(content);
assert!(normalized.contains("# My Title\n\n- Item 1"));
}
#[test]
fn add_blank_line_between_paragraph_and_list() {
let content = "Some paragraph text\n- Item 1\n- Item 2";
let normalized = normalize_loose_lists(content);
assert!(normalized.contains("Some paragraph text\n\n- Item 1"));
}
#[test]
fn preserve_existing_blank_lines() {
let content = "# Title\n\n- Item 1\n\nSome text\n\n- Item 2";
let normalized = normalize_loose_lists(content);
assert_eq!(normalized, content);
}
#[test]
fn handle_loose_lists_with_paragraphs() {
let content = "- Item 1\n\n Paragraph text\n\n- Item 2";
let normalized = normalize_loose_lists(content);
assert!(normalized.contains("- Item 1\n\n Paragraph text\n\n- Item 2"));
}
#[test]
fn multiple_headers_need_spacing() {
let content = "# Header 1\n- Item A\n# Header 2\n- Item B";
let normalized = normalize_loose_lists(content);
assert!(normalized.contains("# Header 1\n\n- Item A"));
assert!(normalized.contains("# Header 2\n\n- Item B"));
}
#[test]
fn ordered_list_child_bullet_preserves_three_space_indent() {
let content = "1. **First item**\n - sub-item a\n - sub-item b";
let normalized = normalize_lists(content);
assert!(
normalized.contains(" - sub-item a"),
"Sub-item must keep 3-space indent under ordered parent. Got:\n{normalized}"
);
assert!(
normalized.contains(" - sub-item b"),
"Sub-item must keep 3-space indent under ordered parent. Got:\n{normalized}"
);
assert!(
!normalized.contains("\n - sub-item"),
"2-space indent must not appear under ordered parent. Got:\n{normalized}"
);
}
#[test]
fn ordered_list_multi_item_children_preserve_indent() {
let content = "1. **Assess Current Stack**\n - What are you using now?\n - How complex is your API?\n\n2. **Second item**\n - sub-item c";
let normalized = normalize_lists(content);
assert!(
normalized.contains(" - What are you using now?"),
"3-space indent must be preserved. Got:\n{normalized}"
);
assert!(
normalized.contains(" - sub-item c"),
"3-space indent must be preserved. Got:\n{normalized}"
);
}
#[test]
fn unordered_list_children_still_use_two_spaces() {
let content = "- Item 1\n - Nested with 4 spaces\n- Item 2";
let normalized = normalize_lists(content);
assert!(
normalized.contains(" - Nested with 4 spaces"),
"Bullet sub-item should normalize to 2 spaces. Got:\n{normalized}"
);
assert!(
!normalized.contains(" - Nested"),
"4-space indent should be reduced. Got:\n{normalized}"
);
}
#[test]
fn double_digit_ordered_list_child_uses_four_space_indent() {
let content = "10. **Item ten**\n - sub-item";
let normalized = normalize_lists(content);
assert!(
normalized.contains(" - sub-item"),
"Sub-item under '10.' must have 4-space indent. Got:\n{normalized}"
);
}
#[test]
fn required_indent_helper_unordered_parent() {
let stack = vec![(0usize, "-".to_string())];
assert_eq!(required_indent_for_level(&stack, 1), " ");
}
#[test]
fn required_indent_helper_ordered_parent() {
let stack = vec![(0usize, "1".to_string())];
assert_eq!(required_indent_for_level(&stack, 1), " ");
}
#[test]
fn required_indent_helper_double_digit_ordered() {
let stack = vec![(0usize, "10".to_string())];
assert_eq!(required_indent_for_level(&stack, 1), " ");
}
#[test]
fn required_indent_helper_nested_ordered_then_unordered() {
let stack = vec![(0usize, "1".to_string()), (3usize, "-".to_string())];
assert_eq!(required_indent_for_level(&stack, 2), " ");
}
#[test]
fn ordered_list_child_after_blank_line_preserves_indent() {
let content = "1. **Assess Current Stack**\n\n - What are you using now?\n - How complex is your API?";
let normalized = normalize_lists(content);
assert!(
normalized.contains(" - What are you using now?"),
"3-space indent must survive blank line before sub-item. Got:\n{normalized}"
);
assert!(
normalized.contains(" - How complex is your API?"),
"3-space indent must survive blank line before sub-item. Got:\n{normalized}"
);
assert!(
!normalized.contains("\n- What are you using now?"),
"Sub-item must not be at column 0 after blank line. Got:\n{normalized}"
);
}
#[test]
fn ordered_list_multiple_items_with_blank_line_children() {
let content = "1. **Assess Current Stack**\n\n - What are you using now?\n - How complex is your API?\n\n2. **Second item**\n\n - sub-item c";
let normalized = normalize_lists(content);
assert!(
normalized.contains(" - What are you using now?"),
"Children of item 1 must keep 3-space indent. Got:\n{normalized}"
);
assert!(
normalized.contains(" - sub-item c"),
"Children of item 2 must keep 3-space indent. Got:\n{normalized}"
);
}
#[test]
fn unordered_lists_separated_by_blank_still_independent() {
let content = "- List A item 1\n- List A item 2\n\nSome paragraph\n\n- List B item 1\n- List B item 2";
let normalized = normalize_lists(content);
assert!(normalized.contains("- List A item 1"));
assert!(normalized.contains("- List B item 1"));
assert!(!normalized.contains(" - List B item 1"));
}
}