diff options
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | README.md | 17 | ||||
-rw-r--r-- | prettytable-evcxr.png | bin | 0 -> 72017 bytes | |||
-rw-r--r-- | src/cell.rs | 95 | ||||
-rw-r--r-- | src/evcxr.rs | 30 | ||||
-rw-r--r-- | src/lib.rs | 94 | ||||
-rw-r--r-- | src/row.rs | 15 | ||||
-rw-r--r-- | src/utils.rs | 38 |
8 files changed, 291 insertions, 2 deletions
@@ -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]]
@@ -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 Binary files differnew file mode 100644 index 0000000..22264be --- /dev/null +++ b/prettytable-evcxr.png diff --git a/src/cell.rs b/src/cell.rs index 795c4c7..410ebd1 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<T: Write + ?Sized>(&self, out: &mut T) -> Result<usize, Error> { + /// 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("<br />"); + out.write_all( + format!( + "<td{1} style=\"{2}\">{0}</td>", + HtmlEscape(&content), + colspan, + styles + ) + .as_bytes(), + )?; + Ok(self.hspan) + } } fn term_error_to_io_error(te: ::term::Error) -> Error { @@ -361,6 +433,25 @@ mod tests { } #[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#"<td style="text-align: left;">hello</td>"#); + } + + #[test] + fn print_html_special_chars() { + let ascii_cell = Cell::new("<abc\">&'"); + + let mut out = StringWriter::new(); + let _ = ascii_cell.print_html(&mut out); + assert_eq!(out.as_string(), r#"<td style="text-align: left;"><abc">&'</td>"#); + } + + #[test] fn align_left() { let cell = Cell::new_align("test", Alignment::LEFT); let mut out = StringWriter::new(); diff --git a/src/evcxr.rs b/src/evcxr.rs new file mode 100644 index 0000000..11d632a --- /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<TableSlice<'a>>, +{ + 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()); + } +} @@ -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<T: Write + ?Sized>(&self, out: &mut T) -> Result<(), Error> { + // Compute column width + let column_num = self.get_column_num(); + out.write_all(b"<table>")?; + // Print titles / table header + if let Some(ref t) = *self.titles { + out.write_all(b"<th>")?; + t.print_html(out, column_num)?; + out.write_all(b"</th>")?; + } + // Print rows + for r in self.rows { + out.write_all(b"<tr>")?; + r.print_html(out, column_num)?; + out.write_all(b"</tr>")?; + } + out.write_all(b"</table>")?; + 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<T: Write + ?Sized>(&self, out: &mut T) -> Result<(), Error> { + self.as_ref().print_html(out) + } } impl Index<usize> 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 = "\ +<table>\ +<th><td style=\"text-align: left;\">t1</td><td style=\"text-align: left;\">t2</td><td style=\"text-align: left;\">t3</td></th>\ +<tr><td style=\"text-align: left;\">a</td><td style=\"text-align: left;\">bc</td><td style=\"text-align: left;\">def</td></tr>\ +<tr><td style=\"text-align: left;\">def</td><td style=\"text-align: left;\">bc</td><td style=\"text-align: left;\">a</td></tr>\ +</table>"; + 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 = "\ +<table>\ +<tr><td style=\"text-align: left;\">a</td><td style=\"text-align: left;\">bc</td><td style=\"text-align: left;\">def</td></tr>\ +<tr><td style=\"text-align: left;\">def</td><td style=\"text-align: left;\">bc</td><td style=\"text-align: left;\">a</td></tr>\ +</table>"; + 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 = "\ +<table>\ +<th><td colspan=\"3\" style=\"text-align: left;\">span horizontal</td></th>\ +<tr><td style=\"font-weight: bold;text-align: left;\">bold</td><td style=\"font-style: italic;text-align: left;\">italic</td><td style=\"text-decoration: underline;text-align: left;\">underline</td></tr>\ +<tr><td style=\"text-align: left;\">left</td><td style=\"text-align: center;\">center</td><td style=\"text-align: right;\">right</td></tr>\ +<tr><td style=\"color: #aa0000;text-align: left;\">red</td><td style=\"color: #000000;text-align: left;\">black</td><td style=\"color: #aa5500;text-align: left;\">yellow</td></tr>\ +<tr><td style=\"color: #ff55ff;background-color: #00aaaa;text-align: left;\">bright magenta on cyan</td><td style=\"color: #aaaaaa;background-color: #55ff55;text-align: left;\">white on bright green</td><td style=\"background-color: #0000aa;text-align: left;\">default on blue</td></tr>\ +</table>"; + let mut writer = StringWriter::new(); + assert!(table.print_html(&mut writer).is_ok()); + assert_eq!(writer.as_string().replace("\r\n", "\n"), out); + } } @@ -205,6 +205,21 @@ impl Row { -> Result<usize, Error> { 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<T: Write + ?Sized>(&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 f2d89ae..dbd8db1 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::*; |