diff options
-rw-r--r-- | src/cell.rs | 95 | ||||
-rw-r--r-- | src/lib.rs | 91 | ||||
-rw-r--r-- | src/row.rs | 15 | ||||
-rw-r--r-- | 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<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(); @@ -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); + } } @@ -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::*; |