mk_ext_prettytable 0.11.0

A library for printing pretty formatted tables in terminal
Documentation
//! Internal only utilities
use std::io::{
  Error,
  ErrorKind,
  Write,
};
use std::{
  fmt,
  str,
};

use unicode_width::{
  UnicodeWidthChar,
  UnicodeWidthStr,
};

use super::format::Alignment;

#[cfg(any(not(windows), not(feature = "win_crlf")))]
pub static NEWLINE: &[u8] = b"\n";
#[cfg(all(windows, feature = "win_crlf"))]
pub static NEWLINE: &[u8] = b"\r\n";

/// Internal utility for writing data into a string
pub struct StringWriter {
  string: String,
}

impl StringWriter {
  /// Create a new `StringWriter`
  pub fn new() -> StringWriter {
    StringWriter {
      string: String::new(),
    }
  }

  /// Return a reference to the internally written `String`
  pub fn as_string(&self) -> &str {
    &self.string
  }
}

impl Write for StringWriter {
  fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
    let string = match str::from_utf8(data) {
      Ok(s) => s,
      Err(e) => {
        return Err(Error::new(
          ErrorKind::Other,
          format!("Cannot decode utf8 string : {}", e),
        ))
      },
    };
    self.string.push_str(string);
    Ok(data.len())
  }

  fn flush(&mut self) -> Result<(), Error> {
    // Nothing to do here
    Ok(())
  }
}

/// Align/fill a string and print it to `out`
/// If `skip_right_fill` is set to `true`, then no space will be added after the string
/// to complete alignment
pub fn print_align<T: Write + ?Sized>(
  out: &mut T,
  align: Alignment,
  text: &str,
  fill: char,
  size: usize,
  skip_right_fill: bool,
) -> Result<(), Error> {
  let text_len = display_width(text);
  let mut nfill = size.saturating_sub(text_len);
  let n = match align {
    Alignment::LEFT => 0,
    Alignment::RIGHT => nfill,
    Alignment::CENTER => nfill / 2,
  };
  if n > 0 {
    out.write_all(&vec![fill as u8; n])?;
    nfill -= n;
  }
  out.write_all(text.as_bytes())?;
  if nfill > 0 && !skip_right_fill {
    out.write_all(&vec![fill as u8; nfill])?;
  }
  Ok(())
}

/// Return the display width of a unicode string.
/// This functions takes ANSI-escaped color codes into account.
pub fn display_width(text: &str) -> usize {
  #[derive(PartialEq, Eq, Clone, Copy)]
  enum State {
    /// We are not inside any terminal escape.
    Normal,
    /// We have just seen a \u{1b}
    EscapeChar,
    /// We have just seen a [
    OpenBracket,
    /// We just ended the escape by seeing an m
    AfterEscape,
  }

  let width = UnicodeWidthStr::width(text);
  let mut state = State::Normal;
  let mut hidden = 0;

  for c in text.chars() {
    state = match (state, c) {
      (State::Normal, '\u{1b}') => State::EscapeChar,
      (State::EscapeChar, '[') => State::OpenBracket,
      (State::EscapeChar, _) => State::Normal,
      (State::OpenBracket, 'm') => State::AfterEscape,
      _ => state,
    };

    // We don't count escape characters as hidden as
    // UnicodeWidthStr::width already considers them.
    if matches!(state, State::OpenBracket | State::AfterEscape) {
      // but if we see an escape char *inside* the ANSI escape, we should ignore it.
      if UnicodeWidthChar::width(c).unwrap_or(0) > 0 {
        hidden += 1;
      }
    }

    if state == State::AfterEscape {
      state = State::Normal;
    }
  }

  assert!(
    width >= hidden,
    "internal error: width {} less than hidden {} on string {:?}",
    width,
    hidden,
    text
  );

  width - hidden
}

/// Wrapper struct which will emit the HTML-escaped version of the contained
/// string when passed to a format string.
pub struct HtmlEscape<'a>(pub &'a str);

impl fmt::Display for HtmlEscape<'_> {
  fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
    // Because the internet is always right, turns out there's not that many
    // characters to escape: http://stackoverflow.com/questions/7381974
    let HtmlEscape(s) = *self;
    let pile_o_bits = s;
    let mut last = 0;
    for (i, ch) in s.bytes().enumerate() {
      match ch as char {
        '<' | '>' | '&' | '\'' | '"' => {
          fmt.write_str(&pile_o_bits[last..i])?;
          let s = match ch as char {
            '>' => "&gt;",
            '<' => "&lt;",
            '&' => "&amp;",
            '\'' => "&#39;",
            '"' => "&quot;",
            _ => unreachable!(),
          };
          fmt.write_str(s)?;
          last = i + 1;
        },
        _ => {},
      }
    }

    if last < s.len() {
      fmt.write_str(&pile_o_bits[last..])?;
    }
    Ok(())
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::format::Alignment;
  use std::io::Write;

  #[test]
  fn string_writer() {
    let mut out = StringWriter::new();
    out.write_all(b"foo").unwrap();
    out.write_all(b" ").unwrap();
    out.write_all(b"").unwrap();
    out.write_all(b"bar").unwrap();
    assert_eq!(out.as_string(), "foo bar");
  }

  #[test]
  fn fill_align() {
    let mut out = StringWriter::new();
    print_align(&mut out, Alignment::RIGHT, "foo", '*', 10, false).unwrap();
    assert_eq!(out.as_string(), "*******foo");

    let mut out = StringWriter::new();
    print_align(&mut out, Alignment::LEFT, "foo", '*', 10, false).unwrap();
    assert_eq!(out.as_string(), "foo*******");

    let mut out = StringWriter::new();
    print_align(&mut out, Alignment::CENTER, "foo", '*', 10, false).unwrap();
    assert_eq!(out.as_string(), "***foo****");

    let mut out = StringWriter::new();
    print_align(&mut out, Alignment::CENTER, "foo", '*', 1, false).unwrap();
    assert_eq!(out.as_string(), "foo");
  }

  #[test]
  fn skip_right_fill() {
    let mut out = StringWriter::new();
    print_align(&mut out, Alignment::RIGHT, "foo", '*', 10, true).unwrap();
    assert_eq!(out.as_string(), "*******foo");

    let mut out = StringWriter::new();
    print_align(&mut out, Alignment::LEFT, "foo", '*', 10, true).unwrap();
    assert_eq!(out.as_string(), "foo");

    let mut out = StringWriter::new();
    print_align(&mut out, Alignment::CENTER, "foo", '*', 10, true).unwrap();
    assert_eq!(out.as_string(), "***foo");

    let mut out = StringWriter::new();
    print_align(&mut out, Alignment::CENTER, "foo", '*', 1, false).unwrap();
    assert_eq!(out.as_string(), "foo");
  }

  #[test]
  fn utf8_error() {
    let mut out = StringWriter::new();
    let res = out.write_all(&[0, 255]);
    assert!(res.is_err());
  }
}