const SIG_SEPARATOR: &str = "-- ";
fn split_quote(line: &str) -> (usize, &str) {
let depth = line.bytes().take_while(|&b| b == b'>').count();
(depth, &line[depth..])
}
pub fn unflow(text: &str, delsp: bool) -> String {
let mut out = String::new();
let mut open: Option<(usize, String)> = None;
for raw in text.lines() {
let (depth, rest) = split_quote(raw);
let rest = rest.strip_prefix(' ').unwrap_or(rest);
let flowed = rest.ends_with(' ') && rest != SIG_SEPARATOR;
let content = match flowed && delsp {
true => &rest[..rest.len() - 1],
false => rest,
};
match &mut open {
Some((d, buf)) if *d == depth => buf.push_str(content),
Some(_) => {
flush(&mut out, open.take());
open = Some((depth, content.to_string()));
}
None => open = Some((depth, content.to_string())),
}
if !flowed {
flush(&mut out, open.take());
}
}
flush(&mut out, open.take());
out
}
fn flush(out: &mut String, paragraph: Option<(usize, String)>) {
let Some((depth, text)) = paragraph else {
return;
};
for _ in 0..depth {
out.push('>');
}
if depth > 0 && !text.is_empty() {
out.push(' ');
}
out.push_str(&text);
out.push('\n');
}
pub fn space_stuff(text: &str) -> String {
let mut out = String::new();
for line in text.lines() {
if line.starts_with(' ') || line.starts_with('>') || line.starts_with("From ") {
out.push(' ');
}
out.push_str(line);
out.push('\n');
}
if !text.ends_with('\n') {
out.pop();
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paragraphs_join_and_fixed_lines_stay() {
let text = "One flowed \nparagraph here.\n\nA fixed line.\n";
assert_eq!(
unflow(text, false),
"One flowed paragraph here.\n\nA fixed line.\n"
);
}
#[test]
fn delsp_drops_the_marking_space() {
assert_eq!(unflow("half \nway\n", true), "halfway\n");
assert_eq!(unflow("half \nway\n", false), "half way\n");
}
#[test]
fn quote_depth_bounds_a_paragraph() {
let text = "> quoted and \n> continued\n>> deeper \n>> line\nmine\n";
assert_eq!(
unflow(text, false),
"> quoted and continued\n>> deeper line\nmine\n"
);
}
#[test]
fn stuffing_is_undone_and_the_signature_stays_fixed() {
assert_eq!(unflow(" indented\n", false), " indented\n");
assert_eq!(unflow(" >not a quote\n", false), ">not a quote\n");
assert_eq!(unflow("text\n-- \nJane\n", false), "text\n-- \nJane\n");
}
#[test]
fn stuffing_marks_the_lines_that_need_it() {
assert_eq!(
space_stuff(" lead\n>quote\nFrom here\nplain\n"),
" lead\n >quote\n From here\nplain\n"
);
assert_eq!(space_stuff("no newline"), "no newline");
}
}