diff --git a/Cargo.toml b/Cargo.toml index a3d0939f3a..b86067654a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,9 @@ keywords = ["tab", "table", "format", "pretty", "print"] categories = ["command-line-interface"] license = "BSD-3-Clause" edition = "2018" +exclude = [ + "prettytable-evcxr.png" +] [badges] appveyor = { repository = "phsym/prettytable-rs", branch = "master", service = "github" } @@ -20,6 +23,7 @@ codecov = { repository = "phsym/prettytable-rs", branch = "master", service = "g [features] default = ["win_crlf", "csv"] +evcxr = [] win_crlf = [] [[bin]] diff --git a/README.md b/README.md index d702cf8921..d1fa0eaa29 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ A formatted and aligned table printer library for [Rust](https://www.rust-lang.o * [Importing](#user-content-importing) * [Exporting](#user-content-exporting) * [Note on line endings](#user-content-note-on-line-endings) + * [Evcxr Integration](#evcxr-integration) ## Including @@ -379,3 +380,19 @@ on any platform. This customization capability will probably move to Formatting API in a future release. Additional examples are provided in the documentation and in [examples](./examples/) directory. + +## Evcxr Integration + +[Evcxr][evcxr] is a Rust REPL and a [Jupyter notebook kernel][evcxr-jupyter]. +This crate integrates into Evcxr and the Jupyter notebooks using the `evcxr` feature flag, which enables native displays of tables. +This includes support for displaying colors and various formattings. + +You can include prettytable as a dependency using this line: +``` +:dep prettytable = { git = "https://github.com/phsym/prettytable-rs", package = "prettytable-rs", features = ["evcxr"] } +``` + +![prettytable being used in a Jupyter notebook with Evcxr Rust kernel.](./prettytable-evcxr.png) + +[evcxr]: https://github.com/google/evcxr/ +[evcxr-jupyter]: https://github.com/google/evcxr/blob/master/evcxr_jupyter/README.md diff --git a/prettytable-evcxr.png b/prettytable-evcxr.png new file mode 100644 index 0000000000..22264bef04 Binary files /dev/null and b/prettytable-evcxr.png differ diff --git a/src/cell.rs b/src/cell.rs index 795c4c750a..410ebd1d17 100644 --- a/src/cell.rs +++ b/src/cell.rs @@ -1,8 +1,7 @@ //! This module contains definition of table/row cells stuff use super::format::Alignment; -use super::utils::display_width; -use super::utils::print_align; +use super::utils::{display_width, print_align, HtmlEscape}; use super::{color, Attr, Terminal}; use std::io::{Error, Write}; use std::string::ToString; @@ -244,6 +243,79 @@ impl Cell { Err(e) => Err(term_error_to_io_error(e)), } } + + /// Print the cell in HTML format to `out`. + pub fn print_html(&self, out: &mut T) -> Result { + /// Convert the color to a hex value useful in CSS + fn color2hex(color: color::Color) -> &'static str { + match color { + color::BLACK => "#000000", + color::RED => "#aa0000", + color::GREEN => "#00aa00", + color::YELLOW => "#aa5500", + color::BLUE => "#0000aa", + color::MAGENTA => "#aa00aa", + color::CYAN => "#00aaaa", + color::WHITE => "#aaaaaa", + color::BRIGHT_BLACK => "#555555", + color::BRIGHT_RED => "#ff5555", + color::BRIGHT_GREEN => "#55ff55", + color::BRIGHT_YELLOW => "#ffff55", + color::BRIGHT_BLUE => "#5555ff", + color::BRIGHT_MAGENTA => "#ff55ff", + color::BRIGHT_CYAN => "#55ffff", + color::BRIGHT_WHITE => "#ffffff", + + // Unknown colors, fallback to blakc + _ => "#000000", + } + }; + + let colspan = if self.hspan > 1 { + format!(" colspan=\"{}\"", self.hspan) + } else { + String::new() + }; + + // Process style properties like color + let mut styles = String::new(); + for style in &self.style { + match style { + Attr::Bold => styles += "font-weight: bold;", + Attr::Italic(true) => styles += "font-style: italic;", + Attr::Underline(true) => styles += "text-decoration: underline;", + Attr::ForegroundColor(c) => { + styles += "color: "; + styles += color2hex(*c); + styles += ";"; + } + Attr::BackgroundColor(c) => { + styles += "background-color: "; + styles += color2hex(*c); + styles += ";"; + } + _ => {} + } + } + // Process alignment + match self.align { + Alignment::LEFT => styles += "text-align: left;", + Alignment::CENTER => styles += "text-align: center;", + Alignment::RIGHT => styles += "text-align: right;", + } + + let content = self.content.join("
"); + out.write_all( + format!( + "{0}", + HtmlEscape(&content), + colspan, + styles + ) + .as_bytes(), + )?; + Ok(self.hspan) + } } fn term_error_to_io_error(te: ::term::Error) -> Error { @@ -360,6 +432,25 @@ mod tests { assert_eq!(out.as_string(), "由系统自动更新 "); } + #[test] + fn print_ascii_html() { + let ascii_cell = Cell::new("hello"); + assert_eq!(ascii_cell.get_width(), 5); + + let mut out = StringWriter::new(); + let _ = ascii_cell.print_html(&mut out); + assert_eq!(out.as_string(), r#"hello"#); + } + + #[test] + fn print_html_special_chars() { + let ascii_cell = Cell::new("&'"); + + let mut out = StringWriter::new(); + let _ = ascii_cell.print_html(&mut out); + assert_eq!(out.as_string(), r#"<abc">&'"#); + } + #[test] fn align_left() { let cell = Cell::new_align("test", Alignment::LEFT); diff --git a/src/evcxr.rs b/src/evcxr.rs new file mode 100644 index 0000000000..11d632acc2 --- /dev/null +++ b/src/evcxr.rs @@ -0,0 +1,30 @@ +//! This modules contains traits and implementations to work within Evcxr + +use super::TableSlice; +use super::utils::StringWriter; +use std::io::Write; + +/// Evcxr specific output trait +pub trait EvcxrDisplay { + /// Print self in one or multiple Evcxr compatile types. + fn evcxr_display(&self); +} + +impl<'a, T> EvcxrDisplay for T +where + T: AsRef>, +{ + fn evcxr_display(&self) { + let mut writer = StringWriter::new(); + // Plain Text + let _ = writer.write_all(b"EVCXR_BEGIN_CONTENT text/plain\n"); + let _ = self.as_ref().print(&mut writer); + let _ = writer.write_all(b"\nEVCXR_END_CONTENT\n"); + + // Html + let _ = writer.write_all(b"EVCXR_BEGIN_CONTENT text/html\n"); + let _ = self.as_ref().print_html(&mut writer); + let _ = writer.write_all(b"\nEVCXR_END_CONTENT\n"); + println!("{}", writer.as_string()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 66daaf9a60..9ccb008083 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,9 @@ mod utils; #[cfg(feature = "csv")] pub mod csv; +#[cfg(feature = "evcxr")] +pub mod evcxr; + pub use row::Row; pub use cell::Cell; use format::{TableFormat, LinePosition, consts}; @@ -204,6 +207,28 @@ impl<'a> TableSlice<'a> { pub fn printstd(&self) -> usize { self.print_tty(false) } + + /// Print table in HTML format to `out`. + pub fn print_html(&self, out: &mut T) -> Result<(), Error> { + // Compute column width + let column_num = self.get_column_num(); + out.write_all(b"")?; + // Print titles / table header + if let Some(ref t) = *self.titles { + out.write_all(b"")?; + } + // Print rows + for r in self.rows { + out.write_all(b"")?; + r.print_html(out, column_num)?; + out.write_all(b"")?; + } + out.write_all(b"
")?; + t.print_html(out, column_num)?; + out.write_all(b"
")?; + out.flush()?; + Ok(()) + } } impl<'a> IntoIterator for &'a TableSlice<'a> { @@ -372,6 +397,10 @@ impl Table { self.as_ref().printstd() } + /// Print table in HTML format to `out`. + pub fn print_html(&self, out: &mut T) -> Result<(), Error> { + self.as_ref().print_html(out) + } } impl Index for Table { @@ -980,4 +1009,69 @@ mod tests { assert_eq!(out, table.to_string().replace("\r\n","\n")); assert_eq!(7, table.print(&mut StringWriter::new()).unwrap()); } + + #[test] + fn table_html() { + let mut table = Table::new(); + table.add_row(Row::new(vec![Cell::new("a"), Cell::new("bc"), Cell::new("def")])); + table.add_row(Row::new(vec![Cell::new("def"), Cell::new("bc"), Cell::new("a")])); + table.set_titles(Row::new(vec![Cell::new("t1"), Cell::new("t2"), Cell::new("t3")])); + let out = "\ +\ +\ +\ +\ +
t1t2t3
abcdef
defbca
"; + let mut writer = StringWriter::new(); + assert!(table.print_html(&mut writer).is_ok()); + assert_eq!(writer.as_string().replace("\r\n", "\n"), out); + table.unset_titles(); + let out = "\ +\ +\ +\ +
abcdef
defbca
"; + let mut writer = StringWriter::new(); + assert!(table.print_html(&mut writer).is_ok()); + assert_eq!(writer.as_string().replace("\r\n", "\n"), out); + } + + #[test] + fn table_html_colors() { + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("bold").style_spec("b"), + Cell::new("italic").style_spec("i"), + Cell::new("underline").style_spec("u"), + ])); + table.add_row(Row::new(vec![ + Cell::new("left").style_spec("l"), + Cell::new("center").style_spec("c"), + Cell::new("right").style_spec("r"), + ])); + table.add_row(Row::new(vec![ + Cell::new("red").style_spec("Fr"), + Cell::new("black").style_spec("Fd"), + Cell::new("yellow").style_spec("Fy"), + ])); + table.add_row(Row::new(vec![ + Cell::new("bright magenta on cyan").style_spec("FMBc"), + Cell::new("white on bright green").style_spec("FwBG"), + Cell::new("default on blue").style_spec("Bb"), + ])); + table.set_titles(Row::new(vec![ + Cell::new("span horizontal").style_spec("H3"), + ])); + let out = "\ +\ +\ +\ +\ +\ +\ +
span horizontal
bolditalicunderline
leftcenterright
redblackyellow
bright magenta on cyanwhite on bright greendefault on blue
"; + let mut writer = StringWriter::new(); + assert!(table.print_html(&mut writer).is_ok()); + assert_eq!(writer.as_string().replace("\r\n", "\n"), out); + } } diff --git a/src/row.rs b/src/row.rs index 94c0d8e5a5..dd3efcfcd7 100644 --- a/src/row.rs +++ b/src/row.rs @@ -205,6 +205,21 @@ impl Row { -> Result { self.__print(out, format, col_width, Cell::print_term) } + + /// Print the row in HTML format to `out`. + /// + /// If the row is has fewer columns than `col_num`, the row is padded with empty cells. + pub fn print_html(&self, out: &mut T, col_num: usize) -> Result<(), Error> { + let mut printed_columns = 0; + for cell in self.iter() { + printed_columns += cell.print_html(out)?; + } + // Pad with empty cells, if target width is not reached + for _ in 0..col_num - printed_columns { + Cell::default().print_html(out)?; + } + Ok(()) + } } impl Default for Row { diff --git a/src/utils.rs b/src/utils.rs index f2d89ae4fe..dbd8db158c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ //! Internal only utilities +use std::fmt; use std::io::{Error, ErrorKind, Write}; use std::str; @@ -105,6 +106,43 @@ pub fn display_width(text: &str) -> usize { 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<'a> fmt::Display for HtmlEscape<'a> { + 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 { + '>' => ">", + '<' => "<", + '&' => "&", + '\'' => "'", + '"' => """, + _ => 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::*;