Skip to content

Commit

Permalink
Merge pull request #101 from jonasbb/html-and-evcxr-integration
Browse files Browse the repository at this point in the history
Html and Evcxr integration
  • Loading branch information
phsym committed Aug 26, 2019
2 parents 1e06ea7 + 1980557 commit bdfb150
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 2 deletions.
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

0 comments on commit bdfb150

Please sign in to comment.