From 917e480a673d1ab03218702d0e9e726131e48799 Mon Sep 17 00:00:00 2001 From: Jonas Bushart Date: Sun, 20 Jan 2019 18:07:20 +0100 Subject: Implement HTML output for Table and TableSlice * Add a HTML escaper to the utils class. * Expand TableSlice, Row, and Cell with HTML printing functions. These are implemented from scratch as they share almost nothing with the line based printing used for text, as HTML directly supports multiline cells. * Add tests to Cell and Table which test the new functionality. --- src/cell.rs | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/row.rs | 15 ++++++++++ src/utils.rs | 38 ++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 2 deletions(-) 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(&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/lib.rs b/src/lib.rs index 66daaf9..1e02055 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,6 +204,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 +394,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 +1006,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 94c0d8e..dd3efcf 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 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::*; -- cgit v1.2.3 From 66b913fa54ca0ce700506125867c3ab117f7667f Mon Sep 17 00:00:00 2001 From: Jonas Bushart Date: Sun, 20 Jan 2019 18:10:23 +0100 Subject: Integrate the new HTML printing with Evcxr * Add optional feature for Evcxr integration * Implement trait for everything which can be converted into a Tableslice. The trait prints the Tableslice in plain-text and HTML format in a Evcxr compatible manner. --- Cargo.toml | 1 + src/evcxr.rs | 30 ++++++++++++++++++++++++++++++ src/lib.rs | 3 +++ 3 files changed, 34 insertions(+) create mode 100644 src/evcxr.rs diff --git a/Cargo.toml b/Cargo.toml index a3d0939..b6be3b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ codecov = { repository = "phsym/prettytable-rs", branch = "master", service = "g [features] default = ["win_crlf", "csv"] +evcxr = [] win_crlf = [] [[bin]] 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>, +{ + 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 1e02055..9ccb008 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}; -- cgit v1.2.3 From 19805573d9260c074f19442b02d62cb8a1769913 Mon Sep 17 00:00:00 2001 From: Jonas Bushart Date: Mon, 26 Aug 2019 00:35:01 +0200 Subject: Add description about the Evcxr support to the readme --- Cargo.toml | 5 ++++- README.md | 17 +++++++++++++++++ prettytable-evcxr.png | Bin 0 -> 72017 bytes 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 prettytable-evcxr.png diff --git a/Cargo.toml b/Cargo.toml index b6be3b1..b860676 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,7 +23,7 @@ codecov = { repository = "phsym/prettytable-rs", branch = "master", service = "g [features] default = ["win_crlf", "csv"] -evcxr = [] +evcxr = [] win_crlf = [] [[bin]] diff --git a/README.md b/README.md index d702cf8..d1fa0ea 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 0000000..22264be Binary files /dev/null and b/prettytable-evcxr.png differ -- cgit v1.2.3