table-format 0.0.5

Formats tables for printing to terminals and logs.
Documentation
use crate::content::{Content, ContentIterator, ContentStyle, CellWidth};
use crate::data_item::DataItem;
use std::clone::Clone;

pub struct TableCellContentIterator<'a> {
    content: &'a Vec<Content>,
    current_content_iterator: ContentIterator,
    current_line_ix: usize,
    base_style: ContentStyle,
    width: usize,
    target_height: usize,
    current_height: usize,
}

impl<'a> Iterator for TableCellContentIterator<'a> {
    type Item = String;

    fn next(&mut self) -> Option<String> {
        let line = 
            if self.current_line_ix < self.content.len() {
                // Get the next line from the current content iterator
                self.current_content_iterator.next().map_or_else(|| {
                    // Go to the next line
                    self.current_line_ix += 1;

                    // If there are more lines, get iterator for next line
                    if self.current_line_ix < self.content.len() {
                        self.current_content_iterator = 
                            self.content[self.current_line_ix].get_iterator(
                                &self.base_style.clone(), self.width);

                        // Get the line from the iterator
                        self.current_content_iterator.next()
                    } else {
                        // No more lines to get
                        None
                    }
                }, Some)
            } else {
                None
            };

        match line {
            Some(content) => {
                Some(content)
            } ,
            None => {
                if self.current_height < self.target_height {
                    // An empty line of spaces the width of the column
                    let result = 
                        (0..self.width)
                            .map(|_| " ")
                            .collect::<String>();
                    self.current_height += 1;
                    Some(result)
                } else {
                    None
                }
            }
        }
    }
}

#[allow(unused_macros)]
#[macro_export]
macro_rules! cell_content {
    ($($out:tt)*) => {
        vec!($($out)*)
    };
}

#[allow(unused_macros)]
#[macro_export]
macro_rules! cell {
    ($style:expr, $($content:tt)*) => {
        Cell::from_styled_content(
            $style, 
            crate::cell_content!($($content)*)
        );
    }
}

/// A table cell represents a single grid rectangle within a table.
///
/// Cells belong to a row.
#[derive(Debug)]
pub struct Cell {
    contents: Vec<Content>,
    base_style: ContentStyle,
}

impl Cell {
    #[must_use]
    pub fn empty() -> Cell {
        Cell {
            contents: Vec::new(),
            base_style: ContentStyle::default(),
        }
    }

    #[must_use]
    pub fn new(
        contents: Vec<Content>,
        base_style: ContentStyle,
    ) -> Cell {
        Cell {
            contents,
            base_style
        }
    }

    /// Returns a `Cell` from styled content.
    ///
    /// # Arguments
    ///
    /// * `format` - The style format.
    /// * `contents` - The contents of the new cell.
    #[must_use]
    pub fn from_styled_content(
        format: &str,
        contents: Vec<&str>,
    ) -> Cell {
        // Split the format string into style tokens
        let styles: Vec<&str> = format.split(' ').collect();
        let mut table_cell = Cell::empty();

        // Iterate the contents
        for (style_ix, content) in contents.into_iter().enumerate() {
            // Get next style (use default if no more styles provided)
            let style = 
                if style_ix < styles.len() { 
                    ContentStyle::from_format(styles[style_ix]) }
                else { ContentStyle::default() };

            // Add the new styled content
            table_cell.contents.push(
                Content::new(content.to_string(), Some(style)));
        }
        table_cell
    }

    /// Returns a `Cell` from a `DataItem`.
    ///
    /// # Arguments
    ///
    /// * `data_item` - The data item from which to build the table cell.
    /// * `base_style` - The base style to apply to the cell contents.
    #[must_use]
    pub fn from_data_item(
        data_item: &DataItem,
        base_style: ContentStyle,
    ) -> Cell {
        Cell::new(
            data_item.lines.clone(),
            base_style,
        )
    }

    /// Returns the column break specified in the first content line of the
    /// cell.
    ///
    /// This is used to determine the column break for cells used in the table
    /// header row.
    #[must_use]
    pub fn get_cell_width(
        self: &Cell
    ) -> CellWidth {
        if self.contents.is_empty() {
            CellWidth::default() }
        else {
            match &self.contents[0].style {
                Some(style) => style.width.clone(),
                None => CellWidth::default()
            }
        }
    }

    /// Returns the next formatted line of content from this table cell.
    ///
    /// # Arguments
    ///
    /// * `self` - The table cell containing the line.
    /// * `width` - The format width.
    #[must_use]
    pub fn get_iterator(
        self: &Cell,
        column_break: &CellWidth
    ) -> TableCellContentIterator {
        // Determine the render width of this cell
        let cell_width = self.measure_width(column_break);

        TableCellContentIterator {
            content: &self.contents,
            current_content_iterator: 
                self.contents[0].get_iterator(&self.base_style.clone(), cell_width),
            current_line_ix: 0,
            base_style: self.base_style.clone(),
            width: cell_width,
            target_height: self.measure_height(column_break),
            current_height: 0
        }
    }

    /// Measures the height needed for this cell when formatting its contents
    ///  into a specific column width.
    ///
    ///  # Arguments
    ///
    /// * `self` - The table cell being measured.
    /// * `column_width` - The column width to measure against.
    #[must_use]
    pub fn measure_height(
        self: &Cell,
        column_break: &CellWidth,
    ) -> usize {
        let mut height = 0;

        // Determine the render width of this cell
        let cell_width = self.measure_width(column_break);

        for line in &self.contents {
            let line_width = line.measure_width();
            // If line fits within column then line height is 1
            if !line.will_wrap() || (line_width <= cell_width) {
                height += 1
            } else {
                // Determine how many lines are needed when content is wrapped
                height += line_width.div_euclid(cell_width) + 1;
            }
        }

        height
    }

    /// Measures the width of this cell.
    ///
    /// # Arguments
    ///
    /// * `self` - The table cell being measured.
    /// * `column_break` - The column break for this cell.
    #[must_use]
    pub fn measure_width(
        self: &Cell,
        column_break: &CellWidth,
    ) -> usize {
        match column_break {
            CellWidth::Fixed(fixed) => *fixed,
            CellWidth::Minimum(minimum_width) => {
                let content_width = self.measure_content_width();
                if minimum_width > &content_width {
                    *minimum_width
                } else {
                    content_width
                }
            },
            CellWidth::Content => {
                self.measure_content_width()
            }
        }
    }

    /// Returns the width of the longest content item in this cell.
    ///
    /// This measure ignores wrapping or truncation and returns the raw width
    ///  of the longest content item.
    fn measure_content_width(
        self: &Cell
    ) -> usize {
        let mut largest = 0;
        for content in &self.contents {
            let content_width = content.measure_width();
            if content_width > largest {
                largest = content_width;
            }
        }

        largest
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use colored::Color;
    use crate::content::{Alignment, Wrap};

    #[test]
    fn test_table_cell_macro() {
        let tc = cell!("{r<;} {G-b^}", "testing", "this");

        assert_eq!(tc.contents.len(), 2);

        assert_eq!(
            format!("{:?}", tc.contents[0]),
            format!("{:?}", Content::new(
                "testing".to_string(),
                Some(ContentStyle::new(
                    Some(Color::Red),
                    None,
                    Alignment::Left,
                    Wrap::Wrap,
                    CellWidth::Content
                ))
            ))
        );
    }
}