summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJonas Bushart <jonas@bushart.org>2019-01-20 18:07:20 +0100
committerJonas Bushart <jonas@bushart.org>2019-08-26 00:36:03 +0200
commit917e480a673d1ab03218702d0e9e726131e48799 (patch)
tree560f4308b71c7addc3bdbc85c024482df2d638e2
parent1e06ea72ccf03cd2129d104360e1e07679190a07 (diff)
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.
-rw-r--r--src/cell.rs95
-rw-r--r--src/lib.rs91
-rw-r--r--src/row.rs15
-rw-r--r--src/utils.rs38
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<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;">&lt;abc&quot;&gt;&amp;&#39;</td>"#);
+ }
+
+ #[test]
fn align_left() {
let cell = Cell::new_align("test", Alignment::LEFT);
let mut out = StringWriter::new();
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<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 +394,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 +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 = "\
+<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);
+ }
}
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<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 {
+ '>' => "&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::*;