Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Html and Evcxr integration #101

Merged
merged 3 commits into from Aug 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Expand Up @@ -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" }
Expand All @@ -20,6 +23,7 @@ codecov = { repository = "phsym/prettytable-rs", branch = "master", service = "g

[features]
default = ["win_crlf", "csv"]
evcxr = []
win_crlf = []

[[bin]]
Expand Down
17 changes: 17 additions & 0 deletions README.md
Expand Up @@ -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

Expand Down Expand Up @@ -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
Binary file added prettytable-evcxr.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 93 additions & 2 deletions 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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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#"<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;">&lt;abc&quot;&gt;&amp;&#39;</td>"#);
}

#[test]
fn align_left() {
let cell = Cell::new_align("test", Alignment::LEFT);
Expand Down
30 changes: 30 additions & 0 deletions 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());
}
}
94 changes: 94 additions & 0 deletions src/lib.rs
Expand Up @@ -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};
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
15 changes: 15 additions & 0 deletions src/row.rs
Expand Up @@ -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 {
Expand Down
38 changes: 38 additions & 0 deletions src/utils.rs
@@ -1,4 +1,5 @@
//! Internal only utilities
use std::fmt;
use std::io::{Error, ErrorKind, Write};
use std::str;

Expand Down Expand Up @@ -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 {
'>' => "&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::*;
Expand Down