use textwrap::{wrap_algorithms::WrapAlgorithm, Options as TwOptions, WordSeparator};
use crate::lang::Fence;
use crate::options::Options;
use crate::prefix::detect_prefix;
pub fn reflow(input: &str, opts: &Options) -> String {
let lines: Vec<&str> = input.split_inclusive('\n').collect();
if lines.is_empty() {
return String::new();
}
let mut out = String::with_capacity(input.len());
let mut fences = FenceStack::new(opts.fences);
let mut i = 0;
while i < lines.len() {
let line = strip_newline(lines[i]);
if fences.consume(line) {
out.push_str(lines[i]);
i += 1;
continue;
}
if line.trim().is_empty() {
out.push_str(lines[i]);
i += 1;
continue;
}
if opts.matches_ignore_marker(line) {
out.push_str(lines[i]);
i += 1;
continue;
}
let prefix = opts
.forced_prefix
.clone()
.unwrap_or_else(|| detect_prefix(line));
let start = i;
while i < lines.len() {
let l = strip_newline(lines[i]);
if l.trim().is_empty() {
break;
}
if opts.matches_ignore_marker(l) {
break;
}
if fences.peek(l) {
break;
}
if detect_prefix(l) != prefix && opts.forced_prefix.is_none() {
break;
}
i += 1;
}
let para_lines: Vec<&str> = lines[start..i].iter().map(|l| strip_newline(l)).collect();
emit_paragraph(¶_lines, &prefix, opts, &mut out);
}
out
}
fn emit_paragraph(lines: &[&str], prefix: &str, opts: &Options, out: &mut String) {
let pure_indent = prefix.chars().all(char::is_whitespace) && prefix.chars().count() >= 4;
if pure_indent {
for l in lines {
out.push_str(l);
out.push('\n');
}
return;
}
if lines.iter().any(|l| opts.matches_skip(l)) {
for l in lines {
out.push_str(l);
out.push('\n');
}
return;
}
let mut prose = String::new();
for l in lines {
let trimmed = l.strip_prefix(prefix).unwrap_or(l).trim();
if !prose.is_empty() {
prose.push(' ');
}
prose.push_str(trimmed);
}
if prose.is_empty() {
for l in lines {
out.push_str(l);
out.push('\n');
}
return;
}
let body_width = opts.width.saturating_sub(prefix.chars().count()).max(10);
let tw = TwOptions::new(body_width)
.wrap_algorithm(WrapAlgorithm::new_optimal_fit())
.word_separator(WordSeparator::AsciiSpace)
.break_words(false);
for line in textwrap::wrap(&prose, &tw) {
out.push_str(prefix);
out.push_str(&line);
out.push('\n');
}
}
struct FenceStack<'a> {
configured: &'a [Fence],
open: Vec<&'a Fence>,
}
impl<'a> FenceStack<'a> {
fn new(configured: &'a [Fence]) -> Self {
Self {
configured,
open: Vec::new(),
}
}
fn consume(&mut self, line: &str) -> bool {
let trimmed = line.trim_start();
if let Some(top) = self.open.last().copied() {
if trimmed.starts_with(top.close) {
self.open.pop();
}
return true;
}
for fence in self.configured {
if trimmed.starts_with(fence.open) {
self.open.push(fence);
return true;
}
}
false
}
fn peek(&self, line: &str) -> bool {
let trimmed = line.trim_start();
if let Some(top) = self.open.last() {
return trimmed.starts_with(top.close);
}
self.configured.iter().any(|f| trimmed.starts_with(f.open))
}
}
fn strip_newline(s: &str) -> &str {
s.strip_suffix('\n').unwrap_or(s)
}