use regex::Regex;
use std::sync::LazyLock;
use super::types::*;
static BLOCKQUOTE_PREFIX_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^((?:\s*>\s*)+)").unwrap());
fn column_at(line: &str, byte_offset: usize) -> usize {
line[..byte_offset].chars().fold(0, column_after)
}
pub(crate) fn list_item_nesting_level(line: &str, item: &ListItemInfo) -> usize {
column_at(line, item.marker_column) / 2
}
pub(crate) fn item_lines_by_list(content: &str, lines: &[LineInfo], block: &ListBlock) -> Vec<Vec<usize>> {
struct OpenList {
kind: u8,
content_column: usize,
quote_depth: usize,
items: Vec<usize>,
}
let mut open: Vec<OpenList> = Vec::new();
let mut lists: Vec<Vec<usize>> = Vec::new();
let mut paragraph_open = false;
for line_num in block.start_line..=block.end_line {
let Some(info) = line_num.checked_sub(1).and_then(|index| lines.get(index)) else {
break;
};
let line = info.content(content);
let quote_depth = info.blockquote.as_ref().map_or(0, |bq| bq.nesting_level);
let item = info
.list_item
.as_deref()
.filter(|_| block.item_lines.binary_search(&line_num).is_ok());
if let Some(item) = item {
let kind = marker_kind(item);
let content_column = column_at(line, item.content_column)
.saturating_sub(quote_origin_column(line, quote_depth).unwrap_or(0));
let continues = |list: &OpenList| {
quote_depth >= list.quote_depth && column_in_quote(line, list.quote_depth) >= list.content_column
};
let mut joins_open_list = false;
if let Some(index) = open.iter().position(|list| !continues(list)) {
lists.extend(open.drain(index + 1..).map(|list| list.items));
let list = &open[index];
joins_open_list = quote_depth == list.quote_depth && list.kind == kind;
if !joins_open_list {
lists.extend(open.pop().map(|list| list.items));
}
}
match open.last_mut() {
Some(list) if joins_open_list => {
list.items.push(line_num);
list.content_column = content_column;
}
_ => open.push(OpenList {
kind,
content_column,
quote_depth,
items: vec![line_num],
}),
}
paragraph_open = !info.in_table_block && item_opens_paragraph(line, item);
continue;
}
let quoted_content_blank = info
.blockquote
.as_ref()
.map_or(info.is_blank, |bq| bq.content.trim().is_empty());
let text = info
.blockquote
.as_ref()
.map_or(line, |bq| bq.content.as_str())
.trim_start();
let starts_a_block = info.in_code_block
|| info.heading.is_some()
|| info.is_horizontal_rule
|| info.in_html_block
|| info.in_table_block
|| crate::utils::html_block::opens_untagged_html_block(text)
|| info.in_math_block
|| info.is_div_marker;
let ends_item = |list: &OpenList| {
if quote_depth < list.quote_depth {
return !paragraph_open || starts_a_block || quoted_content_blank;
}
let interrupts = !paragraph_open || starts_a_block || quote_depth > list.quote_depth;
(quote_depth > list.quote_depth || !quoted_content_blank)
&& interrupts
&& column_in_quote(line, list.quote_depth) < list.content_column
};
if let Some(index) = open.iter().position(ends_item) {
lists.extend(open.drain(index..).map(|list| list.items));
}
paragraph_open = !starts_a_block && !quoted_content_blank;
}
lists.extend(open.into_iter().map(|list| list.items));
lists.sort_by_key(|items| items[0]);
lists
}
fn marker_kind(item: &ListItemInfo) -> u8 {
item.marker.bytes().last().unwrap_or(0)
}
fn item_opens_paragraph(line: &str, item: &ListItemInfo) -> bool {
let mut marker_end = item.marker_column + item.marker.len();
let mut content_start = item.content_column;
loop {
let Some(text) = line.get(content_start..) else {
return false;
};
let text_start = content_start + (text.len() - text.trim_start().len());
let text = text.trim_start();
if text.is_empty() {
return false;
}
if column_at(line, text_start).saturating_sub(column_at(line, marker_end)) >= 5 {
return false;
}
match container_marker_len(text) {
Some(len) => {
marker_end = text_start + len;
content_start = marker_end;
}
None => return !text_opens_block(text),
}
}
}
fn container_marker_len(text: &str) -> Option<usize> {
let bytes = text.as_bytes();
let marker_len = match bytes.first()? {
b'>' => return Some(1),
b'-' | b'+' | b'*' => 1,
b'0'..=b'9' => {
let digits = bytes.iter().take_while(|b| b.is_ascii_digit()).count();
if digits > 9 || !matches!(bytes.get(digits), Some(b'.' | b')')) {
return None;
}
digits + 1
}
_ => return None,
};
bytes
.get(marker_len)
.is_none_or(|b| *b == b' ' || *b == b'\t')
.then_some(marker_len)
}
fn text_opens_block(text: &str) -> bool {
let bytes = text.as_bytes();
let hashes = bytes.iter().take_while(|&&b| b == b'#').count();
let heading = (1..=6).contains(&hashes) && bytes.get(hashes).is_none_or(|b| *b == b' ' || *b == b'\t');
let backticks = bytes.iter().take_while(|&&b| b == b'`').count();
let fence =
(backticks >= 3 && !text[backticks..].contains('`')) || bytes.iter().take_while(|&&b| b == b'~').count() >= 3;
heading
|| fence
|| is_horizontal_rule_content(text.trim_end())
|| crate::utils::html_block::parse_html_block_start(text).is_some()
|| crate::utils::html_block::opens_untagged_html_block(text)
}
fn quote_origin_column(line: &str, quote_depth: usize) -> Option<usize> {
let mut column = 0;
let mut quotes = 0;
let mut chars = line.chars().peekable();
while quotes < quote_depth {
match chars.next()? {
'>' => {
quotes += 1;
column += 1;
}
c @ (' ' | '\t') => column = column_after(column, c),
_ => return None,
}
}
if quote_depth > 0 && matches!(chars.peek(), Some(' ' | '\t')) {
column += 1;
}
Some(column)
}
fn column_in_quote(line: &str, quote_depth: usize) -> usize {
let Some(origin) = quote_origin_column(line, quote_depth) else {
return 0;
};
let mut quotes = 0;
let mut byte = 0;
for c in line.chars() {
match c {
' ' | '\t' => {}
'>' if quotes < quote_depth => quotes += 1,
_ => break,
}
byte += c.len_utf8();
}
column_at(line, byte).saturating_sub(origin)
}
fn column_after(column: usize, c: char) -> usize {
if c == '\t' { (column / 4 + 1) * 4 } else { column + 1 }
}
fn indent_after_blockquote(raw_content: &str, expected_bq_level: usize) -> Option<usize> {
let mut pos = 0;
let mut column = 0;
let mut found_markers = 0;
for c in raw_content.chars() {
pos += c.len_utf8();
column = if c == '\t' { (column / 4 + 1) * 4 } else { column + 1 };
if c == '>' {
found_markers += 1;
if found_markers == expected_bq_level {
break;
}
}
}
if found_markers < expected_bq_level {
return None;
}
let mut prefix_column = column;
match raw_content.get(pos..pos + 1) {
Some(" ") => {
pos += 1;
column += 1;
prefix_column = column;
}
Some("\t") => prefix_column += 1,
_ => {}
}
let after_bq = &raw_content[pos..];
let content_column = after_bq
.chars()
.take_while(|c| c.is_whitespace())
.fold(column, |col, c| if c == '\t' { (col / 4 + 1) * 4 } else { col + 1 });
Some(content_column - prefix_column)
}
pub(super) fn parse_list_blocks(content: &str, lines: &[LineInfo]) -> Vec<ListBlock> {
use crate::utils::code_block_utils::{CodeBlockContext, CodeBlockUtils};
const UNORDERED_LIST_MIN_CONTINUATION_INDENT: usize = 2;
#[inline]
fn reset_tracking_state(
list_item: &ListItemInfo,
has_list_breaking_content: &mut bool,
min_continuation: &mut usize,
) {
*has_list_breaking_content = false;
let marker_width = if list_item.is_ordered {
list_item.marker.len() + 1 } else {
list_item.marker.len()
};
*min_continuation = if list_item.is_ordered {
marker_width
} else {
UNORDERED_LIST_MIN_CONTINUATION_INDENT
};
}
let debug_list = std::env::var("RUMDL_DEBUG_LIST").is_ok();
let mut list_blocks = Vec::with_capacity(lines.len() / 10); let mut current_block: Option<ListBlock> = None;
let mut last_list_item_line = 0;
let mut current_indent_level = 0;
let mut last_marker_width = 0;
let mut has_list_breaking_content_since_last_item = false;
let mut min_continuation_for_tracking = 0;
for (line_idx, line_info) in lines.iter().enumerate() {
let line_num = line_idx + 1;
if line_info.in_code_block && line_info.list_item.is_none() {
if let Some(ref mut block) = current_block {
let min_continuation_indent =
CodeBlockUtils::calculate_min_continuation_indent(content, lines, line_idx);
let context = CodeBlockUtils::analyze_code_block_context(lines, line_idx, min_continuation_indent);
match context {
CodeBlockContext::Indented => {
block.end_line = line_num;
continue;
}
CodeBlockContext::Standalone => {
let completed_block = current_block.take().unwrap();
list_blocks.push(completed_block);
continue;
}
CodeBlockContext::Adjacent => {
block.end_line = line_num;
continue;
}
}
} else {
continue;
}
}
let blockquote_prefix = if let Some(caps) = BLOCKQUOTE_PREFIX_REGEX.captures(line_info.content(content)) {
caps.get(0).unwrap().as_str().to_string()
} else {
String::new()
};
if let Some(ref block) = current_block
&& line_info.list_item.is_none()
&& !line_info.is_blank
&& !line_info.in_code_span_continuation
{
let line_content = line_info.content(content).trim();
let blockquote_prefix_changes = blockquote_prefix.trim() != block.blockquote_prefix.trim();
let breaks_list = line_info.is_valid_heading()
|| line_content.starts_with("---")
|| line_content.starts_with("***")
|| line_content.starts_with("___")
|| (crate::utils::skip_context::is_table_line(line_content)
&& line_info.visual_indent < min_continuation_for_tracking)
|| blockquote_prefix_changes;
if breaks_list {
has_list_breaking_content_since_last_item = true;
}
}
if line_info.in_code_span_continuation
&& line_info.list_item.is_none()
&& let Some(ref mut block) = current_block
{
block.end_line = line_num;
}
let effective_continuation_columns = if let Some(ref block) = current_block {
let block_bq_level = block.blockquote_prefix.chars().filter(|&c| c == '>').count();
let line_content = line_info.content(content);
let line_bq_level = line_content
.chars()
.take_while(|c| *c == '>' || c.is_whitespace())
.filter(|&c| c == '>')
.count();
match indent_after_blockquote(line_content, line_bq_level) {
Some(columns) if line_bq_level > 0 && line_bq_level == block_bq_level => columns,
_ => line_info.visual_indent,
}
} else {
line_info.visual_indent
};
let adjusted_min_continuation_for_tracking = if let Some(ref block) = current_block {
let block_bq_level = block.blockquote_prefix.chars().filter(|&c| c == '>').count();
if block_bq_level > 0 {
if block.is_ordered { last_marker_width } else { 2 }
} else {
min_continuation_for_tracking
}
} else {
min_continuation_for_tracking
};
let inner_content = crate::utils::blockquote::parse_blockquote_prefix(line_info.content(content))
.map_or(line_info.content(content), |parsed| parsed.content)
.trim();
let inside_item = effective_continuation_columns >= adjusted_min_continuation_for_tracking;
let is_structural_element =
opens_own_block(line_info, inner_content, effective_continuation_columns, inside_item)
|| is_horizontal_rule_content(inner_content);
let is_valid_continuation = inside_item || (!line_info.is_blank && !is_structural_element);
if debug_list && line_info.list_item.is_none() && !line_info.is_blank {
eprintln!(
"[DEBUG] Line {}: checking continuation - columns={}, min_cont={}, is_valid={}, in_code_span={}, in_code_block={}, has_block={}",
line_num,
effective_continuation_columns,
adjusted_min_continuation_for_tracking,
is_valid_continuation,
line_info.in_code_span_continuation,
line_info.in_code_block,
current_block.is_some()
);
}
if !line_info.in_code_span_continuation
&& line_info.list_item.is_none()
&& !line_info.is_blank
&& !line_info.in_code_block
&& is_valid_continuation
&& let Some(ref mut block) = current_block
{
if debug_list {
eprintln!(
"[DEBUG] Line {}: extending block.end_line from {} to {}",
line_num, block.end_line, line_num
);
}
block.end_line = line_num;
}
let mut finalize_current_block = false;
if let Some(list_item) = &line_info.list_item {
let item_indent = column_at(line_info.content(content), list_item.marker_column);
let nesting = list_item_nesting_level(line_info.content(content), list_item);
if debug_list {
eprintln!(
"[DEBUG] Line {}: list item found, marker={:?}, indent={}",
line_num, list_item.marker, item_indent
);
}
if let Some(ref mut block) = current_block {
let is_nested = nesting > block.nesting_level;
let same_type =
(block.is_ordered && list_item.is_ordered) || (!block.is_ordered && !list_item.is_ordered);
let same_context = block.blockquote_prefix.trim() == blockquote_prefix.trim();
let reasonable_distance = line_num <= last_list_item_line + 2 || line_num == block.end_line + 1;
let marker_compatible =
block.is_ordered || block.marker.is_none() || block.marker.as_ref() == Some(&list_item.marker);
let has_non_list_content = has_list_breaking_content_since_last_item;
let mut continues_list = if is_nested {
same_context && reasonable_distance && !has_non_list_content
} else {
same_type && same_context && reasonable_distance && marker_compatible && !has_non_list_content
};
if debug_list {
eprintln!(
"[DEBUG] Line {}: continues_list={}, is_nested={}, same_type={}, same_context={}, reasonable_distance={}, marker_compatible={}, has_non_list_content={}, last_item={}, block.end_line={}",
line_num,
continues_list,
is_nested,
same_type,
same_context,
reasonable_distance,
marker_compatible,
has_non_list_content,
last_list_item_line,
block.end_line
);
}
if !continues_list
&& (is_nested || same_type)
&& reasonable_distance
&& line_num > 0
&& block.end_line == line_num - 1
{
continues_list = true;
}
if continues_list {
block.end_line = line_num;
block.item_lines.push(line_num);
block.max_marker_width = block.max_marker_width.max(if list_item.is_ordered {
list_item.marker.len() + 1
} else {
list_item.marker.len()
});
if !block.is_ordered && block.marker.is_some() && block.marker.as_ref() != Some(&list_item.marker) {
block.marker = None;
}
reset_tracking_state(
list_item,
&mut has_list_breaking_content_since_last_item,
&mut min_continuation_for_tracking,
);
} else {
let new_block = ListBlock {
start_line: line_num,
end_line: line_num,
is_ordered: list_item.is_ordered,
marker: if list_item.is_ordered {
None
} else {
Some(list_item.marker.clone())
},
blockquote_prefix: blockquote_prefix.clone(),
item_lines: vec![line_num],
nesting_level: nesting,
max_marker_width: if list_item.is_ordered {
list_item.marker.len() + 1
} else {
list_item.marker.len()
},
};
let old_block = std::mem::replace(block, new_block);
list_blocks.push(old_block);
reset_tracking_state(
list_item,
&mut has_list_breaking_content_since_last_item,
&mut min_continuation_for_tracking,
);
}
} else {
current_block = Some(ListBlock {
start_line: line_num,
end_line: line_num,
is_ordered: list_item.is_ordered,
marker: if list_item.is_ordered {
None
} else {
Some(list_item.marker.clone())
},
blockquote_prefix,
item_lines: vec![line_num],
nesting_level: nesting,
max_marker_width: list_item.marker.len(),
});
reset_tracking_state(
list_item,
&mut has_list_breaking_content_since_last_item,
&mut min_continuation_for_tracking,
);
}
last_list_item_line = line_num;
current_indent_level = item_indent;
last_marker_width = if list_item.is_ordered {
list_item.marker.len() + 1 } else {
list_item.marker.len()
};
} else if let Some(ref mut block) = current_block {
if debug_list {
eprintln!(
"[DEBUG] Line {}: non-list-item, is_blank={}, block exists",
line_num, line_info.is_blank
);
}
let prev_line_ends_with_backslash = if block.end_line > 0 && block.end_line - 1 < lines.len() {
lines[block.end_line - 1].content(content).trim_end().ends_with('\\')
} else {
false
};
let block_bq_level_cont = block.blockquote_prefix.chars().filter(|&c| c == '>').count();
let line_raw_content = line_info.content(content);
let line_bq_level_cont = line_raw_content
.chars()
.take_while(|c| *c == '>' || c.is_whitespace())
.filter(|&c| c == '>')
.count();
let (effective_line_indent, min_continuation_indent) = if block_bq_level_cont > 0
&& line_bq_level_cont == block_bq_level_cont
&& let Some(columns) = indent_after_blockquote(line_raw_content, block_bq_level_cont)
{
let min_indent = if block.is_ordered { last_marker_width } else { 2 };
(columns, min_indent)
} else {
let min_indent = if block.is_ordered {
current_indent_level + last_marker_width
} else {
current_indent_level + 2
};
(line_info.visual_indent, min_indent)
};
if prev_line_ends_with_backslash || effective_line_indent >= min_continuation_indent {
if debug_list {
eprintln!(
"[DEBUG] Line {line_num}: indented continuation (indent={effective_line_indent}, min={min_continuation_indent})",
);
}
block.end_line = line_num;
} else if line_info.is_blank {
if debug_list {
eprintln!("[DEBUG] Line {line_num}: entering blank line handling");
}
let mut check_idx = line_idx + 1;
let mut found_continuation = false;
while check_idx < lines.len() && lines[check_idx].is_blank {
check_idx += 1;
}
if check_idx < lines.len() {
let next_line = &lines[check_idx];
let next_content = next_line.content(content);
let block_bq_level_for_indent = block.blockquote_prefix.chars().filter(|&c| c == '>').count();
let next_bq_level_for_indent = next_content
.chars()
.take_while(|c| *c == '>' || c.is_whitespace())
.filter(|&c| c == '>')
.count();
let effective_indent = if next_bq_level_for_indent > 0
&& next_bq_level_for_indent == block_bq_level_for_indent
&& let Some(columns) = indent_after_blockquote(next_content, next_bq_level_for_indent)
{
columns
} else {
next_line.visual_indent
};
let root_continuation_indent = if block.is_ordered {
block.nesting_level + block.max_marker_width
} else {
block.nesting_level * 2 + 2
};
let adjusted_min_continuation = if block_bq_level_for_indent > 0 {
if block.is_ordered { last_marker_width } else { 2 }
} else {
min_continuation_indent.min(root_continuation_indent)
};
if debug_list {
eprintln!(
"[DEBUG] Blank line {} checking next line {}: effective_indent={}, adjusted_min={}, next_is_list={}, in_code_block={}",
line_num,
check_idx + 1,
effective_indent,
adjusted_min_continuation,
next_line.list_item.is_some(),
next_line.in_code_block
);
}
if effective_indent >= adjusted_min_continuation {
found_continuation = true;
}
else if !next_line.in_code_block
&& next_line.list_item.is_some()
&& let Some(item) = &next_line.list_item
{
let next_blockquote_prefix = BLOCKQUOTE_PREFIX_REGEX
.find(next_line.content(content))
.map_or(String::new(), |m| m.as_str().to_string());
if column_at(next_line.content(content), item.marker_column) == current_indent_level
&& item.is_ordered == block.is_ordered
&& block.blockquote_prefix.trim() == next_blockquote_prefix.trim()
{
let block_bq_level = block.blockquote_prefix.chars().filter(|&c| c == '>').count();
let root_cont = if block.is_ordered {
block.nesting_level + block.max_marker_width
} else {
block.nesting_level * 2 + 2
};
let has_structural_separators = (line_idx + 1..check_idx).any(|idx| {
if let Some(between_line) = lines.get(idx) {
let between_content = between_line.content(content);
let trimmed = between_content.trim();
if trimmed.is_empty() {
return false;
}
let between_bq_prefix = BLOCKQUOTE_PREFIX_REGEX
.find(between_content)
.map_or(String::new(), |m| m.as_str().to_string());
let between_bq_level = between_bq_prefix.chars().filter(|&c| c == '>').count();
let blockquote_level_changed =
trimmed.starts_with('>') && between_bq_level != block_bq_level;
let table_breaks = crate::utils::skip_context::is_table_line(trimmed)
&& between_line.visual_indent < root_cont;
trimmed.starts_with("```")
|| trimmed.starts_with("~~~")
|| trimmed.starts_with("---")
|| trimmed.starts_with("***")
|| trimmed.starts_with("___")
|| blockquote_level_changed
|| table_breaks
|| between_line.is_valid_heading()
} else {
false
}
});
found_continuation = !has_structural_separators;
}
}
}
if debug_list {
eprintln!("[DEBUG] Blank line {line_num} final: found_continuation={found_continuation}");
}
if found_continuation {
block.end_line = line_num;
} else {
finalize_current_block = true;
}
} else {
let mut min_required_indent = if block.is_ordered {
let deep = current_indent_level + last_marker_width;
let root = block.nesting_level + block.max_marker_width;
deep.min(root)
} else {
let deep = current_indent_level + 2;
let root = block.nesting_level * 2 + 2;
deep.min(root)
};
let line_content = line_info.content(content).trim();
let inner_content = crate::utils::blockquote::parse_blockquote_prefix(line_content)
.map_or(line_content, |parsed| parsed.content)
.trim();
let looks_like_table = crate::utils::skip_context::is_table_line(inner_content);
let block_bq_level = block.blockquote_prefix.chars().filter(|&c| c == '>').count();
let current_bq_level = blockquote_prefix.chars().filter(|&c| c == '>').count();
let blockquote_level_changed = line_content.starts_with('>') && current_bq_level != block_bq_level;
let effective_columns = if block_bq_level > 0
&& current_bq_level == block_bq_level
&& !blockquote_level_changed
&& let Some(columns) = indent_after_blockquote(line_info.content(content), block_bq_level)
{
min_required_indent = if block.is_ordered { last_marker_width } else { 2 };
columns
} else {
line_info.visual_indent
};
let inside_item = effective_columns >= min_required_indent;
let is_structural_separator = opens_own_block(line_info, inner_content, effective_columns, inside_item)
|| inner_content.starts_with("---")
|| inner_content.starts_with("***")
|| inner_content.starts_with("___")
|| blockquote_level_changed
|| (looks_like_table && !inside_item);
let is_lazy_continuation = !is_structural_separator && !line_info.is_blank;
if is_lazy_continuation {
block.end_line = line_num;
} else {
finalize_current_block = true;
}
}
}
if finalize_current_block && let Some(block) = current_block.take() {
list_blocks.push(block);
}
}
if let Some(block) = current_block {
list_blocks.push(block);
}
merge_adjacent_list_blocks(content, &mut list_blocks, lines);
list_blocks
}
fn opens_own_block(line_info: &LineInfo, inner_content: &str, indent_columns: usize, inside_item: bool) -> bool {
line_info.is_valid_heading()
|| (!inside_item
&& indent_columns <= 3
&& crate::utils::html_block::parse_html_block_start(inner_content).is_some())
|| inner_content.starts_with("```")
|| inner_content.starts_with("~~~")
}
fn merge_adjacent_list_blocks(content: &str, list_blocks: &mut Vec<ListBlock>, lines: &[LineInfo]) {
if list_blocks.len() < 2 {
return;
}
let mut merger = ListBlockMerger::new(content, lines);
*list_blocks = merger.merge(list_blocks);
}
struct ListBlockMerger<'a> {
content: &'a str,
lines: &'a [LineInfo],
}
impl<'a> ListBlockMerger<'a> {
fn new(content: &'a str, lines: &'a [LineInfo]) -> Self {
Self { content, lines }
}
fn merge(&mut self, list_blocks: &[ListBlock]) -> Vec<ListBlock> {
let mut merged = Vec::with_capacity(list_blocks.len());
let mut current = list_blocks[0].clone();
for next in list_blocks.iter().skip(1) {
if self.should_merge_blocks(¤t, next) {
current = self.merge_two_blocks(current, next);
} else {
merged.push(current);
current = next.clone();
}
}
merged.push(current);
merged
}
fn should_merge_blocks(&self, current: &ListBlock, next: &ListBlock) -> bool {
if !self.blocks_are_compatible(current, next) {
return false;
}
let spacing = self.analyze_spacing_between(current, next);
match spacing {
BlockSpacing::Consecutive => true,
BlockSpacing::SingleBlank => self.can_merge_with_blank_between(current, next),
BlockSpacing::MultipleBlanks | BlockSpacing::ContentBetween => {
self.can_merge_with_content_between(current, next)
}
}
}
fn blocks_are_compatible(&self, current: &ListBlock, next: &ListBlock) -> bool {
current.is_ordered == next.is_ordered
&& current.blockquote_prefix == next.blockquote_prefix
&& current.nesting_level == next.nesting_level
}
fn analyze_spacing_between(&self, current: &ListBlock, next: &ListBlock) -> BlockSpacing {
let gap = next.start_line - current.end_line;
match gap {
1 => BlockSpacing::Consecutive,
2 => BlockSpacing::SingleBlank,
_ if gap > 2 => {
if self.has_only_blank_lines_between(current, next) {
BlockSpacing::MultipleBlanks
} else {
BlockSpacing::ContentBetween
}
}
_ => BlockSpacing::Consecutive, }
}
fn can_merge_with_blank_between(&self, current: &ListBlock, next: &ListBlock) -> bool {
if has_meaningful_content_between(self.content, current, next, self.lines) {
return false; }
!current.is_ordered && current.marker == next.marker
}
fn can_merge_with_content_between(&self, current: &ListBlock, next: &ListBlock) -> bool {
if has_meaningful_content_between(self.content, current, next, self.lines) {
return false; }
current.is_ordered && next.is_ordered
}
fn has_only_blank_lines_between(&self, current: &ListBlock, next: &ListBlock) -> bool {
for line_num in (current.end_line + 1)..next.start_line {
if let Some(line_info) = self.lines.get(line_num - 1)
&& !line_info.content(self.content).trim().is_empty()
{
return false;
}
}
true
}
fn merge_two_blocks(&self, mut current: ListBlock, next: &ListBlock) -> ListBlock {
current.end_line = next.end_line;
current.item_lines.extend_from_slice(&next.item_lines);
current.max_marker_width = current.max_marker_width.max(next.max_marker_width);
if !current.is_ordered && self.markers_differ(¤t, next) {
current.marker = None; }
current
}
fn markers_differ(&self, current: &ListBlock, next: &ListBlock) -> bool {
current.marker.is_some() && next.marker.is_some() && current.marker != next.marker
}
}
#[derive(Debug, PartialEq)]
enum BlockSpacing {
Consecutive, SingleBlank, MultipleBlanks, ContentBetween, }
fn has_meaningful_content_between(content: &str, current: &ListBlock, next: &ListBlock, lines: &[LineInfo]) -> bool {
for line_num in (current.end_line + 1)..next.start_line {
if let Some(line_info) = lines.get(line_num - 1) {
let trimmed = line_info.content(content).trim();
if trimmed.is_empty() {
continue;
}
if line_info.is_valid_heading() {
return true;
}
if is_horizontal_rule_content(trimmed) {
return true;
}
if crate::utils::skip_context::is_table_line(trimmed) {
let min_continuation_indent = if current.is_ordered {
current.nesting_level + current.max_marker_width
} else {
current.nesting_level + 2
};
if line_info.visual_indent < min_continuation_indent {
return true;
}
}
if trimmed.starts_with('>') {
return true;
}
if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
let line_indent = line_info.byte_len - line_info.content(content).trim_start().len();
let min_continuation_indent = if current.is_ordered {
current.nesting_level + current.max_marker_width + 1 } else {
current.nesting_level + 2
};
if line_indent < min_continuation_indent {
return true;
}
}
let line_indent = line_info.byte_len - line_info.content(content).trim_start().len();
let min_indent = if current.is_ordered {
current.nesting_level + current.max_marker_width
} else {
current.nesting_level + 2
};
if line_indent < min_indent {
return true;
}
}
}
false
}
#[cfg(test)]
mod indent_tests {
use super::{column_at, indent_after_blockquote};
#[test]
fn indent_after_blockquote_measures_columns_as_commonmark_does() {
for (line, level, columns) in [
("> foo", 1, 0),
("> foo", 1, 2),
(">\tfoo", 1, 2),
("> \tfoo", 1, 2),
(">\t\tfoo", 1, 6),
("> \tfoo", 1, 2),
("> >\tfoo", 2, 0),
(">>\tfoo", 2, 1),
("> > \tfoo", 2, 4),
(" > \tfoo", 1, 4),
] {
assert_eq!(indent_after_blockquote(line, level), Some(columns), "{line:?}");
}
assert_eq!(indent_after_blockquote("> foo", 2), None);
}
#[test]
fn column_at_expands_tabs_to_the_next_tab_stop() {
for (line, offset, column) in [
("- a", 0, 0),
(" - a", 2, 2),
("\t- a", 1, 4),
(" \t- a", 2, 4),
("\t\t- a", 2, 8),
("> \t- a", 3, 4),
(">\t- a", 2, 4),
] {
assert_eq!(column_at(line, offset), column, "{line:?}");
}
}
}