use std::borrow::Cow;
use code_span_trim::trim_code_span_edge_spaces;
use tracing::trace;
use unicode_width::UnicodeWidthStr;
use super::{inline::wrap_preserving_code, tokenize::parse_open_code_span};
mod code_span_trim;
mod hard_break;
mod pending;
mod spanning_code;
mod tail_reflow;
pub(super) use pending::{
ContinuationMode,
PendingPrefix,
TailReflow,
continuation_prefix_for,
pending_prefix_for_next_segment,
};
#[cfg(test)]
#[path = "paragraph_tests.rs"]
mod tests;
pub(super) fn wraps_to_tail(text: &str, available: usize) -> bool {
wrap_preserving_code(text, available).len() > 1
}
pub(super) fn continuation_folds_tail(continuation_prefix: &str) -> bool {
UnicodeWidthStr::width(continuation_prefix) < 4
&& continuation_prefix
.chars()
.all(|character| character == ' ')
}
pub(super) struct PrefixLine<'a> {
pub(super) prefix: Cow<'a, str>,
pub(super) rest: &'a str,
pub(super) repeat_prefix: bool,
pub(super) outer_prefix: Option<Cow<'a, str>>,
}
#[derive(Default)]
pub(super) struct ParagraphState {
buf: Vec<(String, bool)>,
indent: String,
continuation_indent: Option<String>,
pub(super) pending_prefix: Option<PendingPrefix>,
}
impl ParagraphState {
pub(super) fn clear(&mut self) {
self.buf.clear();
self.indent.clear();
self.continuation_indent = None;
self.pending_prefix = None;
}
pub(super) fn note_indent(&mut self, line: &str) {
if self.buf.is_empty() {
let indent = crate::textproc::leading_indent(line).to_string();
let had_continuation_indent = self.continuation_indent.is_some();
self.indent = self.continuation_indent.take().unwrap_or(indent);
trace!(
line_len = line.len(),
indent_len = self.indent.len(),
had_continuation_indent,
"noting the paragraph indent"
);
}
}
pub(super) fn remember_continuation_indent(&mut self, indent: String) {
self.continuation_indent = Some(indent);
}
pub(super) fn push(&mut self, text: String, hard_break: bool) {
self.buf.push((text, hard_break));
}
pub(super) fn drain_pending_prefix(&mut self) -> Option<PendingPrefix> {
let pending = self.pending_prefix.take()?;
self.buf.clear();
self.indent.clear();
Some(pending)
}
}
pub(super) struct ParagraphWriter<'a> {
out: &'a mut Vec<String>,
width: usize,
}
impl<'a> ParagraphWriter<'a> {
pub(super) fn new(out: &'a mut Vec<String>, width: usize) -> Self { Self { out, width } }
fn wrap_with_prefix(&mut self, prefix: &str, continuation_prefix: &str, text: &str) {
let prefix_width = UnicodeWidthStr::width(prefix);
let available = self.width.saturating_sub(prefix_width).max(1);
let lines = wrap_preserving_code(text, available);
if lines.is_empty() {
self.out.push(prefix.to_string());
return;
}
for (index, wrapped_line) in lines.iter().enumerate() {
if index == 0 {
self.out.push(format!("{prefix}{wrapped_line}"));
} else {
self.out
.push(format!("{continuation_prefix}{wrapped_line}"));
}
}
}
fn append_wrapped_with_prefix(&mut self, line: &PrefixLine<'_>) {
let prefix = line.prefix.as_ref();
let prefix_width = UnicodeWidthStr::width(prefix);
let available = self.width.saturating_sub(prefix_width).max(1);
self.append_wrapped_with_prefix_width(line, available);
}
pub(super) fn append_wrapped_with_prefix_width(
&mut self,
line: &PrefixLine<'_>,
available: usize,
) {
let prefix = line.prefix.as_ref();
let continuation_prefix =
continuation_prefix_for(prefix, line.repeat_prefix, line.outer_prefix.as_deref());
let lines = wrap_preserving_code(line.rest, available);
if lines.is_empty() {
self.out.push(prefix.to_string());
return;
}
for (index, wrapped_line) in lines.iter().enumerate() {
if index == 0 {
self.out.push(format!("{prefix}{wrapped_line}"));
} else {
self.out
.push(format!("{continuation_prefix}{wrapped_line}"));
}
}
}
pub(super) fn emit_pending_with_verbatim_continuation(
&mut self,
pending: PendingPrefix,
continuation: &str,
hard_break: bool,
) {
let prefix = pending.prefix;
let mut first_line = format!("{prefix}{rest}", rest = pending.rest);
if pending.hard_break && !first_line.ends_with(" ") {
first_line.push_str(" ");
}
self.out.push(first_line);
let mut continuation_line = continuation.to_string();
if hard_break && !continuation_line.ends_with(" ") {
continuation_line.push_str(" ");
}
self.out.push(continuation_line);
}
pub(super) fn flush_paragraph(&mut self, state: &mut ParagraphState) {
if let Some(pending) = state.drain_pending_prefix() {
let mut pending = pending;
if pending.continuation_mode == ContinuationMode::VerbatimFlush {
if !pending.repeat_prefix {
state.remember_continuation_indent(continuation_prefix_for(
pending.prefix.as_str(),
pending.repeat_prefix,
pending.outer_prefix.as_deref(),
));
}
self.out.extend(pending.original_lines);
return;
}
let prefix = pending_prefix_for_next_segment(&mut pending);
let rest = trim_code_span_edge_spaces(&pending.rest, &pending.synthetic_join_spaces);
let prefix_line = PrefixLine {
prefix: Cow::Owned(prefix),
rest: rest.as_ref(),
repeat_prefix: pending.repeat_prefix,
outer_prefix: pending.outer_prefix.as_deref().map(Cow::Borrowed),
};
self.append_stable_pending_prefix(&prefix_line, pending.rest_width);
if pending.hard_break {
self.ensure_trailing_hard_break_on_last_line();
}
if !pending.repeat_prefix {
state.remember_continuation_indent(continuation_prefix_for(
pending.prefix.as_str(),
pending.repeat_prefix,
pending.outer_prefix.as_deref(),
));
}
}
if state.buf.is_empty() {
return;
}
if let Some(lines) = spanning_code::conforming_source_lines_for_overlong_span(
&state.buf,
&state.indent,
self.width,
) {
self.out.extend(lines);
state.clear();
return;
}
let mut segment = String::new();
for (text, hard_break) in &state.buf {
if !segment.is_empty() {
segment.push(' ');
}
segment.push_str(text);
if *hard_break {
self.push_wrapped_segment(&state.indent, &segment);
self.ensure_trailing_hard_break_on_last_line();
segment.clear();
}
}
if !segment.is_empty() {
self.push_wrapped_segment(&state.indent, &segment);
}
state.clear();
}
fn push_wrapped_segment(&mut self, indent: &str, segment: &str) {
self.wrap_with_prefix(indent, indent, segment);
}
pub(super) fn push_verbatim(&mut self, state: &mut ParagraphState, line: &str) {
self.flush_paragraph(state);
state.continuation_indent = None;
self.out.push(line.to_string());
}
pub(super) fn handle_prefix_line(
&mut self,
state: &mut ParagraphState,
prefix_line: &PrefixLine<'_>,
) {
self.flush_paragraph(state);
state.continuation_indent = None;
if let Some((fence_len, open_tail)) = parse_open_code_span(prefix_line.rest) {
let opener_at_eol = open_tail.trim().is_empty();
let continuation_mode = if opener_at_eol {
ContinuationMode::TightCodeSpan
} else {
ContinuationMode::Normalize
};
trace!(
?continuation_mode,
opener_at_eol, fence_len, "selected pending-prefix continuation mode"
);
self.defer_prefix_line(state, prefix_line, Some(fence_len), continuation_mode);
return;
}
if self.prefix_line_needs_tail_deferral(prefix_line) {
trace!(
prefix_len = prefix_line.prefix.len(),
rest_len = prefix_line.rest.len(),
"deferring a prefixed line so its tail reflows with the lines below"
);
self.defer_prefix_line(state, prefix_line, None, ContinuationMode::Normalize);
return;
}
self.append_wrapped_with_prefix(prefix_line);
}
}