use tracing::trace;
use super::fragment::{FragmentKind, InlineFragment};
fn is_whitespace_only_line(line: &[InlineFragment]) -> bool {
line.iter().all(InlineFragment::is_whitespace)
}
fn is_single_space_line(line: &[InlineFragment]) -> bool { line.len() == 1 && line[0].text == " " }
fn line_width(line: &[InlineFragment]) -> usize { line.iter().map(|fragment| fragment.width).sum() }
fn line_starts_with_atomic(line: &[InlineFragment]) -> bool {
line.first().is_some_and(InlineFragment::is_atomic)
}
fn line_starts_with_single_space_then_plain(line: &[InlineFragment]) -> bool {
line.first()
.is_some_and(|fragment| fragment.is_whitespace() && fragment.text == " ")
&& line.get(1).is_some_and(InlineFragment::is_plain)
}
fn line_has_rebalanceable_tail(line: &[InlineFragment]) -> bool {
(line.len() > 1 && line.last().is_some_and(InlineFragment::is_atomic))
|| (line.len() > 1 && line.last().is_some_and(InlineFragment::is_plain))
}
fn carry_previous_inline_code_tail(
merged: &mut Vec<Vec<InlineFragment>>,
pending_whitespace: &mut Vec<InlineFragment>,
) -> bool {
let Some(previous_line) = merged.last_mut() else {
return false;
};
if !previous_line
.last()
.is_some_and(|fragment| fragment.kind == FragmentKind::InlineCode)
{
return false;
}
let Some(previous_atomic) = previous_line.pop() else {
debug_assert!(
false,
"inline code tail vanished after successful tail-kind check"
);
return false;
};
pending_whitespace.push(previous_atomic);
if previous_line.is_empty() {
merged.pop();
}
true
}
fn next_content_line(lines: &[Vec<InlineFragment>], index: usize) -> Option<&[InlineFragment]> {
lines
.iter()
.skip(index + 1)
.find(|line| !is_whitespace_only_line(line))
.map(Vec::as_slice)
}
fn inline_code_tail_carry_fits(
merged: &[Vec<InlineFragment>],
next_content_line: Option<&[InlineFragment]>,
width: usize,
) -> bool {
let Some(next_content_line) = next_content_line else {
trace!(
fits = true,
reason = "no following content line",
target_width = width,
"checked inline-code tail carry width"
);
return true;
};
let Some(previous_line) = merged.last() else {
trace!(
fits = true,
reason = "no previous merged line",
target_width = width,
"checked inline-code tail carry width"
);
return true;
};
let Some(previous_tail) = previous_line.last() else {
trace!(
fits = true,
reason = "previous merged line is empty",
target_width = width,
"checked inline-code tail carry width"
);
return true;
};
let next_line_width = line_width(next_content_line);
let projected_width = previous_tail.width + 1 + next_line_width;
let fits = projected_width <= width;
trace!(
fits,
projected_width,
target_width = width,
previous_tail_width = previous_tail.width,
next_line_width,
"checked inline-code tail carry width"
);
fits
}
pub(super) fn merge_whitespace_only_lines(
lines: &[Vec<InlineFragment>],
width: usize,
) -> Vec<Vec<InlineFragment>> {
let mut merged: Vec<Vec<InlineFragment>> = Vec::with_capacity(lines.len());
let mut pending_whitespace: Vec<InlineFragment> = Vec::new();
for (index, mut line) in lines.iter().cloned().enumerate() {
if is_whitespace_only_line(&line) {
trace!(
index,
fragment_count = line.len(),
"normalizing whitespace-only wrapped line"
);
let next_starts_atomic = lines
.get(index + 1)
.is_some_and(|next_line| line_starts_with_atomic(next_line));
let line_is_single_space = is_single_space_line(&line);
let previous_line_has_single_fragment = merged
.last()
.is_some_and(|previous_line| previous_line.len() == 1);
let mut should_carry_whitespace = !line_is_single_space;
if line_is_single_space
&& !next_starts_atomic
&& inline_code_tail_carry_fits(&merged, next_content_line(lines, index), width)
&& carry_previous_inline_code_tail(&mut merged, &mut pending_whitespace)
{
should_carry_whitespace = true;
}
if line_is_single_space && previous_line_has_single_fragment {
should_carry_whitespace = true;
}
if should_carry_whitespace {
pending_whitespace.extend(line);
}
continue;
}
if pending_whitespace.is_empty() {
merged.push(line);
} else {
pending_whitespace.append(&mut line);
merged.push(std::mem::take(&mut pending_whitespace));
}
}
if !pending_whitespace.is_empty() {
if let Some(last_line) = merged.last_mut() {
last_line.append(&mut pending_whitespace);
} else {
merged.push(pending_whitespace);
}
}
merged
}
pub(super) fn rebalance_atomic_tails(lines: &mut [Vec<InlineFragment>], width: usize) {
for index in 0..lines.len().saturating_sub(1) {
if !line_starts_with_single_space_then_plain(&lines[index + 1])
|| !line_has_rebalanceable_tail(&lines[index])
{
continue;
}
let Some(trailing_width) = lines[index].last().map(|fragment| fragment.width) else {
continue;
};
if line_width(&lines[index + 1]) + trailing_width > width {
continue;
}
let Some(trailing_fragment) = lines[index].pop() else {
continue;
};
lines[index + 1].insert(0, trailing_fragment);
}
}
#[cfg(test)]
#[path = "postprocess_tests.rs"]
mod tests;