prettytable/
cell.rs

1//! This module contains definition of table/row cells stuff
2
3use super::format::Alignment;
4use super::utils::{
5  display_width,
6  print_align,
7  HtmlEscape,
8};
9use super::{
10  color,
11  Attr,
12  Terminal,
13};
14use std::io::{
15  Error,
16  Write,
17};
18use std::str::FromStr;
19use std::string::ToString;
20
21/// Represent a table cell containing a string.
22///
23/// Once created, a cell's content cannot be modified.
24/// The cell would have to be replaced by another one
25#[derive(Clone, Debug, Hash, PartialEq, Eq)]
26pub struct Cell {
27  content: Vec<String>,
28  width: usize,
29  align: Alignment,
30  style: Vec<Attr>,
31  hspan: usize,
32}
33
34impl Cell {
35  /// Create a new `Cell` initialized with content from `string`.
36  /// Text alignment in cell is configurable with the `align` argument
37  pub fn new_align(string: &str, align: Alignment) -> Cell {
38    let content: Vec<String> = string.lines().map(|x| x.to_string()).collect();
39    let mut width = 0;
40    for cont in &content {
41      let l = display_width(&cont[..]);
42      if l > width {
43        width = l;
44      }
45    }
46    Cell {
47      content,
48      width,
49      align,
50      style: Vec::new(),
51      hspan: 1,
52    }
53  }
54
55  /// Create a new `Cell` initialized with content from `string`.
56  /// By default, content is align to `LEFT`
57  pub fn new(string: &str) -> Cell {
58    Cell::new_align(string, Alignment::LEFT)
59  }
60
61  /// Set text alignment in the cell
62  pub fn align(&mut self, align: Alignment) {
63    self.align = align;
64  }
65
66  /// Add a style attribute to the cell
67  pub fn style(&mut self, attr: Attr) {
68    self.style.push(attr);
69  }
70
71  /// Add a style attribute to the cell. Can be chained
72  pub fn with_style(mut self, attr: Attr) -> Cell {
73    self.style(attr);
74    self
75  }
76
77  /// Add horizontal spanning to the cell
78  pub fn with_hspan(mut self, hspan: usize) -> Cell {
79    self.set_hspan(hspan);
80    self
81  }
82
83  /// Remove all style attributes and reset alignment to default (LEFT)
84  pub fn reset_style(&mut self) {
85    self.style.clear();
86    self.align(Alignment::LEFT);
87  }
88
89  /// Set the cell's style by applying the given specifier string
90  ///
91  /// # Style spec syntax
92  ///
93  /// The syntax for the style specifier looks like this :
94  /// **FrBybl** which means **F**oreground **r**ed **B**ackground **y**ellow **b**old **l**eft
95  ///
96  /// ### List of supported specifiers :
97  ///
98  /// * **F** : **F**oreground (must be followed by a color specifier)
99  /// * **B** : **B**ackground (must be followed by a color specifier)
100  /// * **H** : **H**orizontal span (must be followed by a number)
101  /// * **b** : **b**old
102  /// * **i** : **i**talic
103  /// * **u** : **u**nderline
104  /// * **c** : Align **c**enter
105  /// * **l** : Align **l**eft
106  /// * **r** : Align **r**ight
107  /// * **d** : **d**efault style
108  ///
109  /// ### List of color specifiers :
110  ///
111  /// * **r** : Red
112  /// * **b** : Blue
113  /// * **g** : Green
114  /// * **y** : Yellow
115  /// * **c** : Cyan
116  /// * **m** : Magenta
117  /// * **w** : White
118  /// * **d** : Black
119  ///
120  /// And capital letters are for **bright** colors.
121  /// Eg :
122  ///
123  /// * **R** : Bright Red
124  /// * **B** : Bright Blue
125  /// * ... and so on ...
126  pub fn style_spec(mut self, spec: &str) -> Cell {
127    self.reset_style();
128    let mut foreground = false;
129    let mut background = false;
130    let mut it = spec.chars().peekable();
131    while let Some(c) = it.next() {
132      if foreground || background {
133        let color = match c {
134          'r' => color::RED,
135          'R' => color::BRIGHT_RED,
136          'b' => color::BLUE,
137          'B' => color::BRIGHT_BLUE,
138          'g' => color::GREEN,
139          'G' => color::BRIGHT_GREEN,
140          'y' => color::YELLOW,
141          'Y' => color::BRIGHT_YELLOW,
142          'c' => color::CYAN,
143          'C' => color::BRIGHT_CYAN,
144          'm' => color::MAGENTA,
145          'M' => color::BRIGHT_MAGENTA,
146          'w' => color::WHITE,
147          'W' => color::BRIGHT_WHITE,
148          'd' => color::BLACK,
149          'D' => color::BRIGHT_BLACK,
150          _ => {
151            // Silently ignore unknown tags
152            foreground = false;
153            background = false;
154            continue;
155          },
156        };
157        if foreground {
158          self.style(Attr::ForegroundColor(color));
159        } else if background {
160          self.style(Attr::BackgroundColor(color));
161        }
162        foreground = false;
163        background = false;
164      } else {
165        match c {
166          'F' => foreground = true,
167          'B' => background = true,
168          'b' => self.style(Attr::Bold),
169          'i' => self.style(Attr::Italic(true)),
170          'u' => self.style(Attr::Underline(true)),
171          'c' => self.align(Alignment::CENTER),
172          'l' => self.align(Alignment::LEFT),
173          'r' => self.align(Alignment::RIGHT),
174          'H' => {
175            let mut span_s = String::new();
176            while let Some('0'..='9') = it.peek() {
177              span_s.push(it.next().unwrap());
178            }
179            let span = usize::from_str(&span_s).unwrap();
180            self.set_hspan(span);
181          },
182          _ => { /* Silently ignore unknown tags */ },
183        }
184      }
185    }
186    self
187  }
188
189  /// Return the height of the cell
190  // #[deprecated(since="0.8.0", note="Will become private in future release. See [issue #87](https://github.com/phsym/prettytable-rs/issues/87)")]
191  pub(crate) fn get_height(&self) -> usize {
192    self.content.len()
193  }
194
195  /// Return the width of the cell
196  // #[deprecated(since="0.8.0", note="Will become private in future release. See [issue #87](https://github.com/phsym/prettytable-rs/issues/87)")]
197  pub(crate) fn get_width(&self) -> usize {
198    self.width
199  }
200
201  /// Set horizontal span for this cell (must be > 0)
202  pub fn set_hspan(&mut self, hspan: usize) {
203    self.hspan = if hspan == 0 { 1 } else { hspan };
204  }
205
206  /// Get horizontal span of this cell (> 0)
207  pub fn get_hspan(&self) -> usize {
208    self.hspan
209  }
210
211  /// Return a copy of the full string contained in the cell
212  pub fn get_content(&self) -> String {
213    self.content.join("\n")
214  }
215
216  /// Print a partial cell to `out`. Since the cell may be multi-lined,
217  /// `idx` is the line index to print. `col_width` is the column width used to
218  /// fill the cells with blanks so it fits in the table.
219  /// If `ìdx` is higher than this cell's height, it will print empty content
220  // #[deprecated(since="0.8.0", note="Will become private in future release. See [issue #87](https://github.com/phsym/prettytable-rs/issues/87)")]
221  pub(crate) fn print<T: Write + ?Sized>(
222    &self,
223    out: &mut T,
224    idx: usize,
225    col_width: usize,
226    skip_right_fill: bool,
227  ) -> Result<(), Error> {
228    let c = self.content.get(idx).map(|s| s.as_ref()).unwrap_or("");
229    print_align(out, self.align, c, ' ', col_width, skip_right_fill)
230  }
231
232  /// Apply style then call `print` to print the cell into a terminal
233  // #[deprecated(since="0.8.0", note="Will become private in future release. See [issue #87](https://github.com/phsym/prettytable-rs/issues/87)")]
234  pub(crate) fn print_term<T: Terminal + ?Sized>(
235    &self,
236    out: &mut T,
237    idx: usize,
238    col_width: usize,
239    skip_right_fill: bool,
240  ) -> Result<(), Error> {
241    for a in &self.style {
242      match out.attr(*a) {
243        Ok(..) | Err(::term::Error::NotSupported) | Err(::term::Error::ColorOutOfRange) => {}, // Ignore unsupported attributes
244        Err(e) => return Err(term_error_to_io_error(e)),
245      };
246    }
247    self.print(out, idx, col_width, skip_right_fill)?;
248    match out.reset() {
249      Ok(..) | Err(::term::Error::NotSupported) | Err(::term::Error::ColorOutOfRange) => Ok(()),
250      Err(e) => Err(term_error_to_io_error(e)),
251    }
252  }
253
254  /// Print the cell in HTML format to `out`.
255  pub fn print_html<T: Write + ?Sized>(&self, out: &mut T) -> Result<usize, Error> {
256    /// Convert the color to a hex value useful in CSS
257    fn color2hex(color: color::Color) -> &'static str {
258      match color {
259        color::BLACK => "#000000",
260        color::RED => "#aa0000",
261        color::GREEN => "#00aa00",
262        color::YELLOW => "#aa5500",
263        color::BLUE => "#0000aa",
264        color::MAGENTA => "#aa00aa",
265        color::CYAN => "#00aaaa",
266        color::WHITE => "#aaaaaa",
267        color::BRIGHT_BLACK => "#555555",
268        color::BRIGHT_RED => "#ff5555",
269        color::BRIGHT_GREEN => "#55ff55",
270        color::BRIGHT_YELLOW => "#ffff55",
271        color::BRIGHT_BLUE => "#5555ff",
272        color::BRIGHT_MAGENTA => "#ff55ff",
273        color::BRIGHT_CYAN => "#55ffff",
274        color::BRIGHT_WHITE => "#ffffff",
275
276        // Unknown colors, fallback to blakc
277        _ => "#000000",
278      }
279    }
280
281    let colspan = if self.hspan > 1 {
282      format!(" colspan=\"{}\"", self.hspan)
283    } else {
284      String::new()
285    };
286
287    // Process style properties like color
288    let mut styles = String::new();
289    for style in &self.style {
290      match style {
291        Attr::Bold => styles += "font-weight: bold;",
292        Attr::Italic(true) => styles += "font-style: italic;",
293        Attr::Underline(true) => styles += "text-decoration: underline;",
294        Attr::ForegroundColor(c) => {
295          styles += "color: ";
296          styles += color2hex(*c);
297          styles += ";";
298        },
299        Attr::BackgroundColor(c) => {
300          styles += "background-color: ";
301          styles += color2hex(*c);
302          styles += ";";
303        },
304        _ => {},
305      }
306    }
307    // Process alignment
308    match self.align {
309      Alignment::LEFT => styles += "text-align: left;",
310      Alignment::CENTER => styles += "text-align: center;",
311      Alignment::RIGHT => styles += "text-align: right;",
312    }
313
314    let content = self.content.join("<br />");
315    out.write_all(
316      format!(
317        "<td{1} style=\"{2}\">{0}</td>",
318        HtmlEscape(&content),
319        colspan,
320        styles
321      )
322      .as_bytes(),
323    )?;
324    Ok(self.hspan)
325  }
326}
327
328fn term_error_to_io_error(te: ::term::Error) -> Error {
329  match te {
330    ::term::Error::Io(why) => why,
331    _ => Error::new(::std::io::ErrorKind::Other, te),
332  }
333}
334
335impl<T: ToString> From<&T> for Cell {
336  fn from(f: &T) -> Cell {
337    Cell::new(&f.to_string())
338  }
339}
340
341#[allow(clippy::to_string_trait_impl)]
342impl ToString for Cell {
343  fn to_string(&self) -> String {
344    self.get_content()
345  }
346}
347
348impl Default for Cell {
349  /// Return a cell initialized with a single empty `String`, with LEFT alignment
350  fn default() -> Cell {
351    Cell {
352      content: vec!["".to_string(); 1],
353      width: 0,
354      align: Alignment::LEFT,
355      style: Vec::new(),
356      hspan: 1,
357    }
358  }
359}
360
361/// This macro simplifies `Cell` creation
362///
363/// Support 2 syntax : With and without style specification.
364/// # Syntax
365/// ```text
366/// cell!(value);
367/// ```
368/// or
369///
370/// ```text
371/// cell!(spec->value);
372/// ```
373/// Value must implement the `std::string::ToString` trait
374///
375/// For details about style specifier syntax, check doc for [`Cell::style_spec`](cell/struct.Cell.html#method.style_spec) method
376/// # Example
377/// ```
378/// # #[macro_use] extern crate prettytable;
379/// # fn main() {
380/// let cell = cell!("value");
381/// // Do something with the cell
382/// # drop(cell);
383/// // Create a cell with style (Red foreground, Bold, aligned to left);
384/// let styled = cell!(Frbl->"value");
385/// # drop(styled);
386/// # }
387/// ```
388#[macro_export]
389macro_rules! cell {
390  () => {
391    $crate::Cell::default()
392  };
393  ($value:expr) => {
394    $crate::Cell::new(&$value.to_string())
395  };
396  ($style:ident -> $value:expr) => {
397    $crate::cell!($value).style_spec(stringify!($style))
398  };
399}
400
401#[cfg(test)]
402mod tests {
403  use super::Cell;
404  use crate::format::Alignment;
405  use crate::utils::StringWriter;
406  use term::{
407    color,
408    Attr,
409  };
410
411  #[test]
412  fn get_content() {
413    let cell = Cell::new("test");
414    assert_eq!(cell.get_content(), "test");
415  }
416
417  #[test]
418  fn print_ascii() {
419    let ascii_cell = Cell::new("hello");
420    assert_eq!(ascii_cell.get_width(), 5);
421
422    let mut out = StringWriter::new();
423    let _ = ascii_cell.print(&mut out, 0, 10, false);
424    assert_eq!(out.as_string(), "hello     ");
425  }
426
427  #[test]
428  fn print_unicode() {
429    let unicode_cell = Cell::new("привет");
430    assert_eq!(unicode_cell.get_width(), 6);
431
432    let mut out = StringWriter::new();
433    let _ = unicode_cell.print(&mut out, 0, 10, false);
434    assert_eq!(out.as_string(), "привет    ");
435  }
436
437  #[test]
438  fn print_cjk() {
439    let unicode_cell = Cell::new("由系统自动更新");
440    assert_eq!(unicode_cell.get_width(), 14);
441    let mut out = StringWriter::new();
442    let _ = unicode_cell.print(&mut out, 0, 20, false);
443    assert_eq!(out.as_string(), "由系统自动更新      ");
444  }
445
446  #[test]
447  fn print_ascii_html() {
448    let ascii_cell = Cell::new("hello");
449    assert_eq!(ascii_cell.get_width(), 5);
450
451    let mut out = StringWriter::new();
452    let _ = ascii_cell.print_html(&mut out);
453    assert_eq!(out.as_string(), r#"<td style="text-align: left;">hello</td>"#);
454  }
455
456  #[test]
457  fn print_html_special_chars() {
458    let ascii_cell = Cell::new("<abc\">&'");
459
460    let mut out = StringWriter::new();
461    let _ = ascii_cell.print_html(&mut out);
462    assert_eq!(
463      out.as_string(),
464      r#"<td style="text-align: left;">&lt;abc&quot;&gt;&amp;&#39;</td>"#
465    );
466  }
467
468  #[test]
469  fn align_left() {
470    let cell = Cell::new_align("test", Alignment::LEFT);
471    let mut out = StringWriter::new();
472    let _ = cell.print(&mut out, 0, 10, false);
473    assert_eq!(out.as_string(), "test      ");
474  }
475
476  #[test]
477  fn align_center() {
478    let cell = Cell::new_align("test", Alignment::CENTER);
479    let mut out = StringWriter::new();
480    let _ = cell.print(&mut out, 0, 10, false);
481    assert_eq!(out.as_string(), "   test   ");
482  }
483
484  #[test]
485  fn align_right() {
486    let cell = Cell::new_align("test", Alignment::RIGHT);
487    let mut out = StringWriter::new();
488    let _ = cell.print(&mut out, 0, 10, false);
489    assert_eq!(out.as_string(), "      test");
490  }
491
492  #[test]
493  fn style_spec() {
494    let mut cell = Cell::new("test").style_spec("FrBBbuic");
495    assert_eq!(cell.style.len(), 5);
496    assert!(cell.style.contains(&Attr::Underline(true)));
497    assert!(cell.style.contains(&Attr::Italic(true)));
498    assert!(cell.style.contains(&Attr::Bold));
499    assert!(cell.style.contains(&Attr::ForegroundColor(color::RED)));
500    assert!(cell.style.contains(&Attr::BackgroundColor(color::BRIGHT_BLUE)));
501    assert_eq!(cell.align, Alignment::CENTER);
502
503    cell = cell.style_spec("FDBwr");
504    assert_eq!(cell.style.len(), 2);
505    assert!(cell.style.contains(&Attr::ForegroundColor(color::BRIGHT_BLACK)));
506    assert!(cell.style.contains(&Attr::BackgroundColor(color::WHITE)));
507    assert_eq!(cell.align, Alignment::RIGHT);
508
509    // Test with invalid sepcifier chars
510    cell = cell.clone();
511    cell = cell.style_spec("FzBr");
512    assert!(cell.style.contains(&Attr::BackgroundColor(color::RED)));
513    assert_eq!(cell.style.len(), 1);
514    cell = cell.style_spec("zzz");
515    assert!(cell.style.is_empty());
516    assert_eq!(cell.get_hspan(), 1);
517    cell = cell.style_spec("FDBwH03r");
518    assert_eq!(cell.get_hspan(), 3);
519  }
520
521  #[test]
522  fn reset_style() {
523    let mut cell = Cell::new("test")
524      .with_style(Attr::ForegroundColor(color::BRIGHT_BLACK))
525      .with_style(Attr::BackgroundColor(color::WHITE));
526    cell.align(Alignment::RIGHT);
527
528    //style_spec("FDBwr");
529    assert_eq!(cell.style.len(), 2);
530    assert_eq!(cell.align, Alignment::RIGHT);
531    cell.reset_style();
532    assert_eq!(cell.style.len(), 0);
533    assert_eq!(cell.align, Alignment::LEFT);
534  }
535
536  #[test]
537  fn default_empty_cell() {
538    let cell = Cell::default();
539    assert_eq!(cell.align, Alignment::LEFT);
540    assert!(cell.style.is_empty());
541    assert_eq!(cell.get_content(), "");
542    assert_eq!(cell.to_string(), "");
543    assert_eq!(cell.get_height(), 1);
544    assert_eq!(cell.get_width(), 0);
545  }
546}