use std::cmp::min;
use std::io::Write;
pub(crate) struct NewlineCollapser<W> {
max_newlines: usize,
underlying: W,
current_newline_stretch: Option<usize>,
}
impl<W> NewlineCollapser<W>
where
W: Write,
{
pub(crate) fn new(underlying: W, max_newlines: usize) -> Self {
Self {
max_newlines,
underlying,
current_newline_stretch: None,
}
}
pub(crate) fn have_pending_newlines(&self) -> bool {
match self.current_newline_stretch {
None | Some(0) => false,
Some(_) => true,
}
}
pub(crate) fn take_underlying(self) -> W {
self.underlying
}
fn flush_newlines(&mut self) -> std::io::Result<()> {
if let Some(newlines) = self.current_newline_stretch {
for _ in 0..min(newlines, self.max_newlines) {
writeln!(self.underlying)?;
}
}
self.current_newline_stretch = Some(0);
Ok(())
}
fn increment_newline_stretch(&mut self) {
self.current_newline_stretch = Some(match self.current_newline_stretch {
None => 0,
Some(n) => n + 1,
});
}
}
impl<W: Write> Write for NewlineCollapser<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let mut wrote = 0;
let mut remaining = buf;
while !remaining.is_empty() {
match memchr::memchr(b'\n', remaining) {
None => {
self.flush_newlines()?;
wrote += self.underlying.write(remaining)?;
self.current_newline_stretch = Some(0);
break;
}
Some(0) => {
self.increment_newline_stretch();
wrote += 1; remaining = &remaining[1..];
}
Some(n) => {
self.flush_newlines()?;
let underlying_wrote_n = self.underlying.write(&remaining[..n])?;
wrote += underlying_wrote_n;
if underlying_wrote_n == n {
self.increment_newline_stretch();
wrote += 1;
}
remaining = &remaining[underlying_wrote_n + 1..];
}
}
}
Ok(wrote)
}
fn flush(&mut self) -> std::io::Result<()> {
self.underlying.flush()
}
}
#[cfg(test)]
mod test {
use crate::output::fmt_plain_writer::NewlineCollapser;
use std::io::Write;
#[test]
fn no_newlines() {
check(1, ["hello"], "hello");
}
#[test]
fn empty() {
check(1, [""], "");
}
#[test]
fn start_with_newlines() {
check(1, ["\nA", "\nB", "\n", "\nC", "\n", "\n", "D"], "A\nB\nC\nD");
}
#[test]
fn end_with_newlines() {
check(1, ["A\n", "B\n\n", "C\n"], "A\nB\nC");
}
#[test]
fn newlines_in_middle() {
check(1, ["A\nB", "C\n\nD"], "A\nBC\nD");
}
#[test]
fn collapse_stretches_more_than_two() {
check(2, ["A\nB\n\nC\n\n\nD"], "A\nB\n\nC\n\nD");
}
#[test]
fn trailing_newlines_always_trimmed() {
check(3, ["A\n\n\n\n\n"], "A");
}
fn check<const N: usize>(max_newlines: usize, inputs: [&str; N], expect: &str) {
let input_lens: usize = inputs.iter().map(|s| s.len()).sum();
let mut collapser = NewlineCollapser::new(Vec::with_capacity(expect.len()), max_newlines);
let mut wrote = 0;
for input in inputs {
let bs = input.as_bytes();
wrote += collapser.write(bs).expect("should have written");
}
let actual_str = String::from_utf8(collapser.take_underlying()).expect("utf8 encoding problem");
assert_eq!(&actual_str, expect);
assert_eq!(wrote, input_lens);
}
}