Repository: zhiburt/tabled
Pull request #310 (2023-03-19 10:40:37)

Refactorings


Patch:
diff --git a/csv_to_table/COPYING b/csv_to_table/COPYING
deleted file mode 100644
index b99732a..0000000
--- a/csv_to_table/COPYING
+++ /dev/null
@@ -1,3 +0,0 @@
-This project is dual-licensed under the Unlicense and MIT licenses.
-
-You may use this code under the terms of either license.
\ No newline at end of file
diff --git a/json_to_table/COPYING b/json_to_table/COPYING
deleted file mode 100644
index b99732a..0000000
--- a/json_to_table/COPYING
+++ /dev/null
@@ -1,3 +0,0 @@
-This project is dual-licensed under the Unlicense and MIT licenses.
-
-You may use this code under the terms of either license.
\ No newline at end of file
diff --git a/papergrid/COPYING b/papergrid/COPYING
deleted file mode 100644
index b99732a..0000000
--- a/papergrid/COPYING
+++ /dev/null
@@ -1,3 +0,0 @@
-This project is dual-licensed under the Unlicense and MIT licenses.
-
-You may use this code under the terms of either license.
\ No newline at end of file
diff --git a/papergrid/Cargo.toml b/papergrid/Cargo.toml
index c3b5598..c1708a5 100644
--- a/papergrid/Cargo.toml
+++ b/papergrid/Cargo.toml
@@ -13,7 +13,7 @@ std = []
 color = ["ansi-str", "ansitok"]
 
 [dependencies]
-unicode-width = "0.1.9"
+unicode-width = "0.1.10"
 bytecount = "0.6.3"
 fnv = "1.0.7"
 ansi-str = { version = "0.7.2", optional = true }
diff --git a/papergrid/src/color/ansi_color.rs b/papergrid/src/color/ansi_color.rs
index cf1b6af..021e9d2 100644
--- a/papergrid/src/color/ansi_color.rs
+++ b/papergrid/src/color/ansi_color.rs
@@ -68,9 +68,10 @@ impl std::convert::TryFrom<String> for AnsiColor<'static> {
 fn parse_ansi_color(s: &str) -> Option<AnsiColor<'static>> {
     let mut blocks = ansi_str::get_blocks(s);
     let block = blocks.next()?;
+    let style = block.style();
 
-    let start = block.style().start().to_string();
-    let end = block.style().end().to_string();
+    let start = style.start().to_string();
+    let end = style.end().to_string();
 
     Some(AnsiColor::new(start.into(), end.into()))
 }
diff --git a/papergrid/src/dimension/compact.rs b/papergrid/src/dimension/compact.rs
index 505a856..bb8b88c 100644
--- a/papergrid/src/dimension/compact.rs
+++ b/papergrid/src/dimension/compact.rs
@@ -31,6 +31,11 @@ impl CompactGridDimension {
     pub fn width<R: Records>(records: R, cfg: &CompactConfig) -> Vec<usize> {
         build_width(records, cfg)
     }
+
+    /// Calculates dimensions of columns.
+    pub fn dimension<R: Records>(records: R, cfg: &CompactConfig) -> (Vec<usize>, Vec<usize>) {
+        build_dims(records, cfg)
+    }
 }
 
 impl Dimension for CompactGridDimension {
@@ -54,6 +59,25 @@ where
     }
 }
 
+fn build_dims<R: Records>(records: R, cfg: &CompactConfig) -> (Vec<usize>, Vec<usize>) {
+    let mut heights = vec![];
+    let mut widths = vec![0; records.count_columns()];
+
+    for columns in records.iter_rows() {
+        let mut row_height = 0;
+        for (col, cell) in columns.into_iter().enumerate() {
+            let height = get_cell_height(cell.as_ref(), cfg);
+            let width = get_cell_width(cell.as_ref(), cfg);
+            row_height = max(row_height, height);
+            widths[col] = max(widths[col], width)
+        }
+
+        heights.push(row_height);
+    }
+
+    (widths, heights)
+}
+
 fn build_height<R: Records>(records: R, cfg: &CompactConfig) -> Vec<usize> {
     let mut heights = vec![];
 
@@ -83,14 +107,15 @@ fn build_width<R: Records>(records: R, cfg: &CompactConfig) -> Vec<usize> {
 }
 
 fn get_cell_height(cell: &str, cfg: &CompactConfig) -> usize {
-    let padding = cfg.get_padding();
     let count_lines = max(1, count_lines(cell));
-    count_lines + padding.top.size + padding.bottom.size
+    let pad = cfg.get_padding();
+
+    count_lines + pad.top.size + pad.bottom.size
 }
 
 fn get_cell_width(text: &str, cfg: &CompactConfig) -> usize {
-    let pad = cfg.get_padding();
     let width = string_width_multiline(text);
+    let pad = cfg.get_padding();
 
     width + pad.left.size + pad.right.size
 }
diff --git a/papergrid/src/records/vec_records/cell_info.rs b/papergrid/src/records/vec_records/cell_info.rs
index 40af1a4..826edcf 100644
--- a/papergrid/src/records/vec_records/cell_info.rs
+++ b/papergrid/src/records/vec_records/cell_info.rs
@@ -21,6 +21,11 @@ impl<S> CellInfo<S> {
     {
         create_cell_info(text)
     }
+
+    /// Return a original text value.
+    pub fn into_inner(self) -> S {
+        self.text
+    }
 }
 
 impl<S> AsRef<str> for CellInfo<S>
diff --git a/static_table/COPYING b/static_table/COPYING
deleted file mode 100644
index b99732a..0000000
--- a/static_table/COPYING
+++ /dev/null
@@ -1,3 +0,0 @@
-This project is dual-licensed under the Unlicense and MIT licenses.
-
-You may use this code under the terms of either license.
\ No newline at end of file
diff --git a/table_to_html/COPYING b/table_to_html/COPYING
deleted file mode 100644
index b99732a..0000000
--- a/table_to_html/COPYING
+++ /dev/null
@@ -1,3 +0,0 @@
-This project is dual-licensed under the Unlicense and MIT licenses.
-
-You may use this code under the terms of either license.
\ No newline at end of file
diff --git a/tabled/COPYING b/tabled/COPYING
deleted file mode 100644
index b99732a..0000000
--- a/tabled/COPYING
+++ /dev/null
@@ -1,3 +0,0 @@
-This project is dual-licensed under the Unlicense and MIT licenses.
-
-You may use this code under the terms of either license.
\ No newline at end of file
diff --git a/tabled/Cargo.toml b/tabled/Cargo.toml
index 9a439a4..dd170c6 100644
--- a/tabled/Cargo.toml
+++ b/tabled/Cargo.toml
@@ -256,7 +256,7 @@ papergrid = { path = "../papergrid", version = "0.7.1", default-features = false
 tabled_derive = { path = "../tabled_derive", version = "0.5.0", optional = true }
 ansi-str = { version = "0.7.2", optional = true }
 ansitok = { version = "0.2.0", optional = true }
-unicode-width = "0.1.9"
+unicode-width = "0.1.10"
 
 [dev-dependencies]
 owo-colors = "3.5.0"
diff --git a/tabled/src/builder/table_builder.rs b/tabled/src/builder/table_builder.rs
index 5117171..acee454 100644
--- a/tabled/src/builder/table_builder.rs
+++ b/tabled/src/builder/table_builder.rs
@@ -1,6 +1,6 @@
 use std::iter::FromIterator;
 
-use crate::Table;
+use crate::{grid::records::vec_records::CellInfo, Table};
 
 use super::IndexBuilder;
 
@@ -38,9 +38,9 @@ use super::IndexBuilder;
 #[derive(Debug, Default, Clone)]
 pub struct Builder {
     /// A list of rows.
-    records: Vec<Vec<String>>,
+    data: Vec<Vec<CellInfo<String>>>,
     /// A columns row.
-    columns: Option<Vec<String>>,
+    columns: Option<Vec<CellInfo<String>>>,
     /// A number of columns.
     count_columns: usize,
     /// A flag that the rows are not consistent.
@@ -58,7 +58,7 @@ impl Builder {
     /// Creates a [`Builder`] instance.
     pub fn with_capacity(capacity: usize) -> Self {
         let mut b = Self::new();
-        b.records = Vec::with_capacity(capacity);
+        b.data = Vec::with_capacity(capacity);
 
         b
     }
@@ -201,7 +201,7 @@ impl Builder {
         let list = create_row(row, self.count_columns);
 
         self.update_size(list.len());
-        self.records.push(list);
+        self.data.push(list);
     }
 
     /// Insert a row into a specific position.
@@ -217,7 +217,7 @@ impl Builder {
         let list = create_row(record, self.count_columns);
 
         self.update_size(list.len());
-        self.records.insert(index, list);
+        self.data.insert(index, list);
 
         true
     }
@@ -265,7 +265,7 @@ impl Builder {
 
     /// Returns an amount of rows which would be present in a built table.
     pub fn count_rows(&self) -> usize {
-        self.records.len()
+        self.data.len()
     }
 
     /// Checks whether a builder contains a header set.
@@ -279,16 +279,17 @@ impl Builder {
             let col = col - i;
 
             let mut is_empty = true;
-            for row in 0..self.records.len() {
-                if !self.records[row][col].is_empty() {
+            for row in 0..self.data.len() {
+                let cell = &self.data[row][col];
+                if !cell.as_ref().is_empty() {
                     is_empty = false;
                     break;
                 }
             }
 
             if is_empty {
-                for row in 0..self.records.len() {
-                    let _ = self.records[row].remove(col);
+                for row in 0..self.data.len() {
+                    let _ = self.data[row].remove(col);
                 }
 
                 if let Some(columns) = self.columns.as_mut() {
@@ -305,17 +306,18 @@ impl Builder {
     }
 
     fn clean_rows(&mut self) {
-        for row in (0..self.records.len()).rev() {
+        for row in (0..self.data.len()).rev() {
             let mut is_empty = true;
             for col in 0..self.count_columns {
-                if !self.records[row][col].is_empty() {
+                let cell = &self.data[row][col];
+                if !cell.as_ref().is_empty() {
                     is_empty = false;
                     break;
                 }
             }
 
             if is_empty {
-                let _ = self.records.remove(row);
+                let _ = self.data.remove(row);
             }
 
             if row == 0 {
@@ -325,26 +327,28 @@ impl Builder {
     }
 
     fn update_size(&mut self, size: usize) {
+        use std::cmp::Ordering;
+
         match size.cmp(&self.count_columns) {
-            std::cmp::Ordering::Less => {
-                if !self.records.is_empty() {
+            Ordering::Less => {
+                if !self.data.is_empty() {
                     self.is_consistent = false;
                 }
             }
-            std::cmp::Ordering::Greater => {
+            Ordering::Greater => {
                 self.count_columns = size;
 
-                if !self.records.is_empty() || self.columns.is_some() {
+                if !self.data.is_empty() || self.columns.is_some() {
                     self.is_consistent = false;
                 }
             }
-            std::cmp::Ordering::Equal => (),
+            Ordering::Equal => (),
         }
     }
 
     fn get_size(&mut self) -> usize {
         let mut max = self.columns.as_ref().map_or(0, Vec::len);
-        let max_records = self.records.iter().map(Vec::len).max().unwrap_or(0);
+        let max_records = self.data.iter().map(Vec::len).max().unwrap_or(0);
         max = std::cmp::max(max_records, max);
 
         max
@@ -352,18 +356,19 @@ impl Builder {
 
     fn fix_rows(&mut self) {
         let empty_cell = self.empty_cell_text.to_owned().unwrap_or_default();
+        let empty = CellInfo::new(empty_cell);
 
         if let Some(header) = self.columns.as_mut() {
             if self.count_columns > header.len() {
                 let count = self.count_columns - header.len();
-                append_vec(header, empty_cell.clone(), count);
+                append_vec(header, empty.clone(), count);
             }
         }
 
-        for row in &mut self.records {
+        for row in &mut self.data {
             if self.count_columns > row.len() {
                 let count = self.count_columns - row.len();
-                append_vec(row, empty_cell.clone(), count);
+                append_vec(row, empty.clone(), count);
             }
         }
     }
@@ -375,9 +380,29 @@ impl From<Builder> for Vec<Vec<String>> {
             builder.fix_rows();
         }
 
-        append_header(&mut builder.records, builder.columns);
+        if let Some(columns) = builder.columns {
+            builder.data.insert(0, columns);
+        }
+
+        builder
+            .data
+            .into_iter()
+            .map(|row| row.into_iter().map(|c| c.into_inner()).collect())
+            .collect()
+    }
+}
+
+impl From<Builder> for Vec<Vec<CellInfo<String>>> {
+    fn from(mut builder: Builder) -> Self {
+        if !builder.is_consistent {
+            builder.fix_rows();
+        }
+
+        if let Some(columns) = builder.columns {
+            builder.data.insert(0, columns);
+        }
 
-        builder.records
+        builder.data
     }
 }
 
@@ -406,11 +431,16 @@ where
 }
 
 impl From<Vec<Vec<String>>> for Builder {
-    fn from(records: Vec<Vec<String>>) -> Self {
-        let count_columns = records.get(0).map_or(0, |row| row.len());
+    fn from(data: Vec<Vec<String>>) -> Self {
+        let count_columns = data.get(0).map_or(0, |row| row.len());
+
+        let data = data
+            .into_iter()
+            .map(|row| row.into_iter().map(CellInfo::new).collect())
+            .collect();
 
         Self {
-            records,
+            data,
             count_columns,
             columns: None,
             is_consistent: false,
@@ -419,25 +449,35 @@ impl From<Vec<Vec<String>>> for Builder {
     }
 }
 
-fn create_row<R, T>(row: R, size: usize) -> Vec<String>
+impl From<Vec<Vec<CellInfo<String>>>> for Builder {
+    fn from(data: Vec<Vec<CellInfo<String>>>) -> Self {
+        let count_columns = data.get(0).map_or(0, |row| row.len());
+
+        Self {
+            data,
+            count_columns,
+            columns: None,
+            is_consistent: false,
+            empty_cell_text: None,
+        }
+    }
+}
+
+fn create_row<R, T>(row: R, size: usize) -> Vec<CellInfo<String>>
 where
     R: IntoIterator<Item = T>,
     T: Into<String>,
 {
     let mut list = Vec::with_capacity(size);
     for text in row {
-        list.push(text.into());
+        let text = text.into();
+        let info = CellInfo::new(text);
+        list.push(info);
     }
 
     list
 }
 
-fn append_header(records: &mut Vec<Vec<String>>, columns: Option<Vec<String>>) {
-    if let Some(columns) = columns {
-        records.insert(0, columns);
-    }
-}
-
 fn append_vec<T: Clone>(v: &mut Vec<T>, value: T, n: usize) {
     v.extend((0..n).map(|_| value.clone()));
 }
diff --git a/tabled/src/grid/dimension/mod.rs b/tabled/src/grid/dimension/mod.rs
index 825a285..38e2fae 100644
--- a/tabled/src/grid/dimension/mod.rs
+++ b/tabled/src/grid/dimension/mod.rs
@@ -14,7 +14,7 @@ mod static_dimension;
 pub use self::{
     complete_dimension::CompleteDimension,
     peekable_dimension::PeekableDimension,
-    static_dimension::{ExactList, StaticDimension},
+    static_dimension::{DimensionValue, StaticDimension},
 };
 pub use const_dimension::{ConstDimension, ConstSize};
 pub use papergrid::dimension::{Dimension, Estimate};
diff --git a/tabled/src/grid/dimension/static_dimension.rs b/tabled/src/grid/dimension/static_dimension.rs
index 6142e59..f9474f2 100644
--- a/tabled/src/grid/dimension/static_dimension.rs
+++ b/tabled/src/grid/dimension/static_dimension.rs
@@ -3,18 +3,18 @@ use crate::grid::dimension::{Dimension, Estimate};
 /// A constant dimension.
 #[derive(Debug, Clone)]
 pub struct StaticDimension {
-    width: ExactList,
-    height: ExactList,
+    width: DimensionValue,
+    height: DimensionValue,
 }
 
 impl StaticDimension {
     /// Creates a constant dimension.
-    pub fn new(width: ExactList, height: ExactList) -> Self {
+    pub fn new(width: DimensionValue, height: DimensionValue) -> Self {
         Self { width, height }
     }
 }
 
-impl From<StaticDimension> for (ExactList, ExactList) {
+impl From<StaticDimension> for (DimensionValue, DimensionValue) {
     fn from(value: StaticDimension) -> Self {
         (value.width, value.height)
     }
@@ -36,19 +36,28 @@ impl<R, C> Estimate<R, C> for StaticDimension {
 
 /// A dimension value.
 #[derive(Debug, Clone)]
-pub enum ExactList {
+pub enum DimensionValue {
     /// Const width value.
     Exact(usize),
     /// A list of width values for columns.
     List(Vec<usize>),
+    /// A list of width values for columns and a value for the rest.
+    Partial(Vec<usize>, usize),
 }
 
-impl ExactList {
+impl DimensionValue {
     /// Get a width by column.
     pub fn get(&self, col: usize) -> usize {
         match self {
-            ExactList::Exact(val) => *val,
-            ExactList::List(cols) => cols[col],
+            DimensionValue::Exact(val) => *val,
+            DimensionValue::List(cols) => cols[col],
+            DimensionValue::Partial(cols, val) => {
+                if cols.len() > col {
+                    cols[col]
+                } else {
+                    *val
+                }
+            }
         }
     }
 }
diff --git a/tabled/src/tables/iter.rs b/tabled/src/tables/iter.rs
index 4b616b2..1c84217 100644
--- a/tabled/src/tables/iter.rs
+++ b/tabled/src/tables/iter.rs
@@ -26,13 +26,13 @@
 //!
 //! [`Table`]: crate::Table
 
-use std::{cmp, fmt, io};
+use std::{fmt, io};
 
 use crate::{
     grid::{
         colors::NoColors,
         config::{AlignmentHorizontal, CompactConfig, Indent, Sides, SpannedConfig},
-        dimension::{CompactGridDimension, Dimension, ExactList, StaticDimension},
+        dimension::{CompactGridDimension, DimensionValue, StaticDimension},
         records::{
             into_records::{
                 truncate_records::ExactValue, BufColumns, BufRows, LimitColumns, LimitRows,
@@ -56,7 +56,6 @@ use super::util::utf8_writer::UTF8Writer;
 pub struct IterTable<I> {
     records: I,
     cfg: CompactConfig,
-    dim: StaticDimension,
     table: Settings,
 }
 
@@ -65,6 +64,8 @@ struct Settings {
     sniff: usize,
     count_columns: Option<usize>,
     count_rows: Option<usize>,
+    width: Option<usize>,
+    height: Option<usize>,
 }
 
 impl<I> IterTable<I> {
@@ -76,11 +77,12 @@ impl<I> IterTable<I> {
         Self {
             records: iter,
             cfg: create_config(),
-            dim: StaticDimension::new(ExactList::Exact(0), ExactList::Exact(1)),
             table: Settings {
                 sniff: 1000,
                 count_columns: None,
                 count_rows: None,
+                height: None,
+                width: None,
             },
         }
     }
@@ -92,7 +94,8 @@ impl<I> IterTable<I> {
     {
         let count_columns = self.table.count_columns.unwrap_or(0);
         let mut records = IterRecords::new(&self.records, count_columns, self.table.count_rows);
-        option.change(&mut records, &mut self.cfg, &mut self.dim);
+        let mut dims = StaticDimension::new(DimensionValue::Exact(0), DimensionValue::Exact(1));
+        option.change(&mut records, &mut self.cfg, &mut dims);
 
         self
     }
@@ -117,19 +120,13 @@ impl<I> IterTable<I> {
 
     /// Set a height for each row.
     pub fn height(mut self, size: usize) -> Self {
-        let pad = self.cfg.get_padding();
-        let pad = pad.top.size + pad.bottom.size;
-        let (w, _) = self.dim.into();
-        self.dim = StaticDimension::new(w, ExactList::Exact(size + pad));
+        self.table.height = Some(size);
         self
     }
 
     /// Set a width for each column.
     pub fn width(mut self, size: usize) -> Self {
-        let pad = self.cfg.get_padding();
-        let pad = pad.left.size + pad.right.size;
-        let (_, h) = self.dim.into();
-        self.dim = StaticDimension::new(ExactList::Exact(size + pad), h);
+        self.table.width = Some(size);
         self
     }
 
@@ -148,9 +145,10 @@ impl<I> IterTable<I> {
     }
 
     /// Format table into [`io::Write`]r.
-    pub fn build<W: io::Write>(self, writer: W) -> io::Result<()>
+    pub fn build<W>(self, writer: W) -> io::Result<()>
     where
         I: IntoRecords,
+        W: io::Write,
     {
         let writer = UTF8Writer::new(writer);
         self.fmt(writer)
@@ -158,113 +156,170 @@ impl<I> IterTable<I> {
     }
 
     /// Format table into [fmt::Write]er.
-    pub fn fmt<W: fmt::Write>(self, writer: W) -> fmt::Result
+    pub fn fmt<W>(self, writer: W) -> fmt::Result
     where
         I: IntoRecords,
+        W: fmt::Write,
     {
-        let (width, height) = self.dim.into();
-        let width = exact_list_to_exact_value(width);
-        let height = exact_list_to_exact_value(height);
-        let dims = Dims::new(width, height);
-
-        build_grid(writer, self.records, self.cfg, &self.table, dims)
+        build_grid(writer, self.records, self.cfg, self.table)
     }
 }
 
 fn build_grid<W: fmt::Write, I: IntoRecords>(
-    writer: W,
-    records: I,
-    config: CompactConfig,
-    iter_cfg: &Settings,
-    dims: Dims<'_>,
-) -> Result<(), fmt::Error> {
-    let count_rows = iter_cfg.count_rows;
-
-    let padding = config.get_padding();
-    let padding = padding.left.size + padding.right.size;
-
-    let dont_sniff =
-        !matches!(dims.width, ExactValue::Exact(0)) && iter_cfg.count_columns.is_some();
+    f: W,
+    iter: I,
+    cfg: CompactConfig,
+    opts: Settings,
+) -> fmt::Result {
+    let dont_sniff = opts.width.is_some() && opts.count_columns.is_some();
     if dont_sniff {
-        let count_columns = iter_cfg.count_columns.unwrap();
-
-        let content_width = match &dims.width {
-            ExactValue::Exact(w) => ExactValue::Exact(w.saturating_sub(padding)),
-            ExactValue::List(list) => {
-                ExactValue::List(list.iter().map(|w| w.saturating_sub(padding)).collect())
-            }
-        };
-
-        match count_rows {
-            Some(limit) => {
-                let records = LimitRows::new(records, limit);
-                let records = build_records(records, content_width, count_columns, count_rows);
-                let cfg = SpannedConfig::from(config);
-                return Grid::new(records, dims, cfg, NoColors).build(writer);
-            }
-            None => {
-                let records = build_records(records, content_width, count_columns, count_rows);
-                let cfg = SpannedConfig::from(config);
-                return Grid::new(records, dims, cfg, NoColors).build(writer);
-            }
+        build_table_with_static_dims(f, iter, cfg, opts)
+    } else if opts.width.is_none() {
+        build_table_sniffing_with_unknown_width(f, iter, cfg, opts)
+    } else {
+        build_table_sniffing_with_known_width(f, iter, cfg, opts)
+    }
+}
+
+fn build_table_with_static_dims<W, I>(
+    f: W,
+    iter: I,
+    cfg: CompactConfig,
+    opts: Settings,
+) -> fmt::Result
+where
+    W: fmt::Write,
+    I: IntoRecords,
+{
+    let count_columns = opts.count_columns.unwrap();
+    let width = opts.width.unwrap();
+    let height = opts.height.unwrap_or(1);
+    let contentw = ExactValue::Exact(width);
+    let pad = cfg.get_padding();
+    let w = DimensionValue::Exact(width + pad.left.size + pad.right.size);
+    let h = DimensionValue::Exact(height + pad.top.size + pad.bottom.size);
+    let dims = StaticDimension::new(w, h);
+    let cfg = SpannedConfig::from(cfg);
+
+    match opts.count_rows {
+        Some(limit) => {
+            let records = LimitRows::new(iter, limit);
+            let records = build_records(records, contentw, count_columns, Some(limit));
+            Grid::new(records, dims, cfg, NoColors).build(f)
+        }
+        None => {
+            let records = build_records(iter, contentw, count_columns, None);
+            Grid::new(records, dims, cfg, NoColors).build(f)
         }
     }
+}
 
-    let records = BufRows::new(records, iter_cfg.sniff);
+fn build_table_sniffing_with_unknown_width<W, I>(
+    f: W,
+    iter: I,
+    cfg: CompactConfig,
+    opts: Settings,
+) -> fmt::Result
+where
+    W: fmt::Write,
+    I: IntoRecords,
+{
+    let records = BufRows::new(iter, opts.sniff);
     let records = BufColumns::from(records);
 
-    let count_columns = match iter_cfg.count_columns {
-        Some(size) => size,
-        None => records
-            .as_slice()
-            .iter()
-            .map(|row| row.len())
-            .max()
-            .unwrap_or(0),
+    let count_columns = get_count_columns(&opts, records.as_slice());
+
+    let (mut width, height) = {
+        let records = LimitColumns::new(records.as_slice(), count_columns);
+        let records = IterRecords::new(records, count_columns, None);
+        CompactGridDimension::dimension(records, &cfg)
     };
 
-    let (contentw, dimsw) = match dims.width {
-        ExactValue::Exact(0) => {
-            let records = LimitColumns::new(records.as_slice(), count_columns);
-            let records = IterRecords::new(records, count_columns, None);
-            let width = CompactGridDimension::width(records, &config);
+    let padding = cfg.get_padding();
+    let pad = padding.left.size + padding.right.size;
+    let padv = padding.top.size + padding.bottom.size;
+
+    if opts.sniff == 0 {
+        width = std::iter::repeat(pad)
+            .take(count_columns)
+            .collect::<Vec<_>>();
+    }
 
-            let dims_width = width.iter().map(|i| cmp::max(*i, padding)).collect();
-            let content_width = width.iter().map(|i| i.saturating_sub(padding)).collect();
+    let content_width = ExactValue::List(width.iter().map(|i| i.saturating_sub(pad)).collect());
+    let dims_width = DimensionValue::List(width);
+
+    let height_exact = opts.height.unwrap_or(1) + padv;
+    let mut dims_height = DimensionValue::Partial(height, height_exact);
+
+    if opts.height.is_some() {
+        dims_height = DimensionValue::Exact(height_exact);
+    }
 
-            (
-                ExactValue::List(content_width),
-                ExactValue::List(dims_width),
-            )
+    let dims = StaticDimension::new(dims_width, dims_height);
+    let cfg = SpannedConfig::from(cfg);
+
+    match opts.count_rows {
+        Some(limit) => {
+            let records = LimitRows::new(records, limit);
+            let records = build_records(records, content_width, count_columns, Some(limit));
+            Grid::new(records, dims, cfg, NoColors).build(f)
         }
-        width => {
-            let content_width = match &width {
-                ExactValue::Exact(w) => ExactValue::Exact(w.saturating_sub(padding)),
-                ExactValue::List(list) => {
-                    ExactValue::List(list.iter().map(|w| w.saturating_sub(padding)).collect())
-                }
-            };
-
-            (content_width, width)
+        None => {
+            let records = build_records(records, content_width, count_columns, None);
+            Grid::new(records, dims, cfg, NoColors).build(f)
         }
-    };
+    }
+}
+
+fn build_table_sniffing_with_known_width<W, I>(
+    f: W,
+    iter: I,
+    cfg: CompactConfig,
+    opts: Settings,
+) -> fmt::Result
+where
+    W: fmt::Write,
+    I: IntoRecords,
+{
+    let records = BufRows::new(iter, opts.sniff);
+    let records = BufColumns::from(records);
+
+    let count_columns = get_count_columns(&opts, records.as_slice());
+
+    let width = opts.width.unwrap();
+    let contentw = ExactValue::Exact(width);
+
+    let padding = cfg.get_padding();
+    let pad = padding.left.size + padding.right.size;
+    let padv = padding.top.size + padding.bottom.size;
 
-    let dimension = Dims::new(dimsw, dims.height);
-    match count_rows {
+    let height = opts.height.unwrap_or(1) + padv;
+    let dimsh = DimensionValue::Exact(height);
+    let dimsw = DimensionValue::Exact(width + pad);
+    let dims = StaticDimension::new(dimsw, dimsh);
+
+    let cfg = SpannedConfig::from(cfg);
+
+    match opts.count_rows {
         Some(limit) => {
             let records = LimitRows::new(records, limit);
-            let records = build_records(records, contentw, count_columns, count_rows);
-            let cfg = SpannedConfig::from(config);
-            Grid::new(records, dimension, cfg, NoColors).build(writer)
+            let records = build_records(records, contentw, count_columns, Some(limit));
+            Grid::new(records, dims, cfg, NoColors).build(f)
         }
         None => {
-            let records = build_records(records, contentw, count_columns, count_rows);
-            let cfg = SpannedConfig::from(config);
-            Grid::new(records, dimension, cfg, NoColors).build(writer)
+            let records = build_records(records, contentw, count_columns, None);
+            Grid::new(records, dims, cfg, NoColors).build(f)
         }
     }
 }
 
+fn get_count_columns(opts: &Settings, buf: &[Vec<String>]) -> usize {
+    match opts.count_columns {
+        Some(size) => size,
+        None => buf.iter().map(|row| row.len()).max().unwrap_or(0),
+    }
+}
+
 fn create_config() -> CompactConfig {
     CompactConfig::default()
         .set_padding(Sides::new(
@@ -287,32 +342,3 @@ fn build_records<I: IntoRecords>(
     let records = LimitColumns::new(records, count_columns);
     IterRecords::new(records, count_columns, count_rows)
 }
-
-#[derive(Debug, Clone)]
-struct Dims<'a> {
-    width: ExactValue<'a>,
-    height: ExactValue<'a>,
-}
-
-impl<'a> Dims<'a> {
-    fn new(width: ExactValue<'a>, height: ExactValue<'a>) -> Self {
-        Self { width, height }
-    }
-}
-
-impl Dimension for Dims<'_> {
-    fn get_width(&self, column: usize) -> usize {
-        self.width.get(column)
-    }
-
-    fn get_height(&self, row: usize) -> usize {
-        self.height.get(row)
-    }
-}
-
-fn exact_list_to_exact_value(width: ExactList) -> ExactValue<'static> {
-    match width {
-        ExactList::Exact(w) => ExactValue::Exact(w),
-        ExactList::List(list) => ExactValue::List(list.into()),
-    }
-}
diff --git a/tabled/src/tables/table.rs b/tabled/src/tables/table.rs
index ab0c671..158b90d 100644
--- a/tabled/src/tables/table.rs
+++ b/tabled/src/tables/table.rs
@@ -279,11 +279,7 @@ where
 
 impl From<Builder> for Table {
     fn from(builder: Builder) -> Self {
-        let data: Vec<Vec<String>> = builder.into();
-        let data = data
-            .into_iter()
-            .map(|row| row.into_iter().map(CellInfo::new).collect())
-            .collect();
+        let data: Vec<Vec<CellInfo<String>>> = builder.into();
         let records = VecRecords::new(data);
 
         Self {
diff --git a/tabled_derive/COPYING b/tabled_derive/COPYING
deleted file mode 100644
index b99732a..0000000
--- a/tabled_derive/COPYING
+++ /dev/null
@@ -1,3 +0,0 @@
-This project is dual-licensed under the Unlicense and MIT licenses.
-
-You may use this code under the terms of either license.
\ No newline at end of file


Test patch:
diff --git a/csv_to_table/tests/iter.rs b/csv_to_table/tests/iter.rs
index 9c39b4c..41fc84e 100644
--- a/csv_to_table/tests/iter.rs
+++ b/csv_to_table/tests/iter.rs
@@ -18,7 +18,6 @@ Year,Industry_aggregation_NZSIOC,Industry_name_NZSIOC,Units,Variable_code,Variab
 2021,Level 1,Machinary,Dollars,H09,Interest and donations,Financial performance,"26,138"
 "#;
 
-#[cfg(test)]
 #[test]
 fn test_iter() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_1.trim().as_bytes());
@@ -44,7 +43,6 @@ fn test_iter() {
     )
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_width() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_1.trim().as_bytes()).width(2);
@@ -71,7 +69,6 @@ fn test_iter_width() {
     )
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_width_zero() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_1.trim().as_bytes()).width(0);
@@ -98,7 +95,6 @@ fn test_iter_width_zero() {
     )
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_width_and_cols() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_1.trim().as_bytes())
@@ -127,7 +123,6 @@ fn test_iter_width_and_cols() {
     )
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_cols() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_1.trim().as_bytes()).columns(5);
@@ -154,7 +149,6 @@ fn test_iter_cols() {
     )
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_height() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_1.trim().as_bytes()).height(2);
@@ -187,7 +181,6 @@ fn test_iter_height() {
     )
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_rows() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_1.trim().as_bytes()).rows(2);
@@ -206,7 +199,6 @@ fn test_iter_rows() {
     )
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_rows_cols() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_1.trim().as_bytes())
@@ -227,7 +219,6 @@ fn test_iter_rows_cols() {
     )
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_rows_cols_zero() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_1.trim().as_bytes())
@@ -239,7 +230,6 @@ fn test_iter_rows_cols_zero() {
     assert_eq!(table, "")
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_rows_zero() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_1.trim().as_bytes()).rows(0);
@@ -249,7 +239,6 @@ fn test_iter_rows_zero() {
     assert_eq!(table, "")
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_1() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_2.trim().as_bytes());
@@ -276,7 +265,6 @@ fn test_iter_1() {
     );
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_sniff() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_2.trim().as_bytes()).sniff(1);
@@ -303,7 +291,6 @@ fn test_iter_sniff() {
     );
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_sniff_zero() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_2.trim().as_bytes()).sniff(0);
@@ -313,7 +300,6 @@ fn test_iter_sniff_zero() {
     assert_eq!(table, "");
 }
 
-#[cfg(test)]
 #[test]
 fn test_iter_sniff_zero_cols() {
     let table = csv_to_table::iter::from_reader(CSV_DATA_2.trim().as_bytes())
diff --git a/tabled/tests/iter_table.rs b/tabled/tests/iter_table.rs
index a4d36c5..1f24a5f 100644
--- a/tabled/tests/iter_table.rs
+++ b/tabled/tests/iter_table.rs
@@ -7,7 +7,7 @@ use util::{create_matrix, test_table};
 
 test_table!(
     iter_table,
-    IterTable::new(create_matrix::<3, 3>()).to_string(),
+    IterTable::new(create_matrix::<3, 3>()),
     "+-----+-----+-----+"
     "| 0-0 | 0-1 | 0-2 |"
     "+-----+-----+-----+"
@@ -19,7 +19,7 @@ test_table!(
 
 test_table!(
     iter_table_cols,
-    IterTable::new(create_matrix::<3, 3>()).columns(3).to_string(),
+    IterTable::new(create_matrix::<3, 3>()).columns(3),
     "+-----+-----+-----+"
     "| 0-0 | 0-1 | 0-2 |"
     "+-----+-----+-----+"
@@ -31,7 +31,7 @@ test_table!(
 
 test_table!(
     iter_table_cols_less,
-    IterTable::new(create_matrix::<3, 3>()).columns(2).to_string(),
+    IterTable::new(create_matrix::<3, 3>()).columns(2),
     "+-----+-----+"
     "| 0-0 | 0-1 |"
     "+-----+-----+"
@@ -43,9 +43,7 @@ test_table!(
 
 test_table!(
     iter_table_cols_zero,
-    IterTable::new(create_matrix::<3, 3>())
-        .columns(0)
-        .to_string(),
+    IterTable::new(create_matrix::<3, 3>()).columns(0),
     ""
 );
 
@@ -67,7 +65,7 @@ test_table!(
 
 test_table!(
     iter_table_width,
-    IterTable::new(create_matrix::<3, 3>()).width(2).to_string(),
+    IterTable::new(create_matrix::<3, 3>()).width(2),
     "+----+----+----+"
     "| 0- | 0- | 0- |"
     "+----+----+----+"
@@ -79,7 +77,7 @@ test_table!(
 
 test_table!(
     iter_table_height_does_not_work,
-    IterTable::new(create_matrix::<3, 3>()).height(5).to_string(),
+    IterTable::new(create_matrix::<3, 3>()).height(5),
     "+-----+-----+-----+"
     "| 0-0 | 0-1 | 0-2 |"
     "|     |     |     |"
@@ -101,6 +99,59 @@ test_table!(
     "+-----+-----+-----+"
 );
 
+test_table!(
+    iter_table_sniff_0,
+    IterTable::new(create_matrix::<3, 3>()).sniff(0),
+    ""
+);
+
+test_table!(
+    iter_table_multiline,
+    IterTable::new(
+        vec![
+            vec!["0", "1", "2", "3"],
+            vec!["0\n1\n2\n3\n4", "0\n1\n2\n\n\n3\n4", "0\n1\n2\n3\n4\n\n\n", "0\n1\n2\n\n\n3\n4\n"]
+        ]
+    ),
+    "+---+---+---+---+"
+    "| 0 | 1 | 2 | 3 |"
+    "+---+---+---+---+"
+    "| 0 | 0 | 0 | 0 |"
+    "| 1 | 1 | 1 | 1 |"
+    "| 2 | 2 | 2 | 2 |"
+    "| 3 |   | 3 |   |"
+    "| 4 |   | 4 |   |"
+    "|   | 3 |   | 3 |"
+    "|   | 4 |   | 4 |"
+    "|   |   |   |   |"
+    "+---+---+---+---+"
+);
+
+test_table!(
+    iter_table_multiline_sniff_1,
+    IterTable::new(
+        vec![
+            vec!["0", "1", "2", "3"],
+            vec!["0\n1\n2\n3\n4", "0\n1\n2\n\n\n3\n4", "0\n1\n2\n3\n4\n\n\n", "0\n1\n2\n\n\n3\n4\n"]
+        ]
+    )
+    .sniff(1),
+    "+---+---+---+---+\n| 0 | 1 | 2 | 3 |\n+---+---+---+---+\n| 0\n1\n2\n3\n4 | 0\n1\n2\n\n\n3\n4 | 0\n1\n2\n3\n4\n\n\n | 0\n1\n2\n\n\n3\n4\n |\n+---+---+---+---+"
+);
+
+test_table!(
+    iter_table_multiline_sniff_2,
+    IterTable::new(
+        vec![
+            vec!["0", "1", "2", "3"],
+            vec!["0\n1\n2\n3\n4", "0\n1\n2\n\n\n3\n4", "0\n1\n2\n3\n4\n\n\n", "0\n1\n2\n\n\n3\n4\n"],
+            vec!["0\n1\n2\n3\n4", "0\n1\n2\n\n\n3\n4", "0\n1\n2\n3\n4\n\n\n", "0\n1\n2\n\n\n3\n4\n"],
+        ]
+    )
+    .sniff(2),
+    "+---+---+---+---+\n| 0 | 1 | 2 | 3 |\n+---+---+---+---+\n| 0 | 0 | 0 | 0 |\n| 1 | 1 | 1 | 1 |\n| 2 | 2 | 2 | 2 |\n| 3 |   | 3 |   |\n| 4 |   | 4 |   |\n|   | 3 |   | 3 |\n|   | 4 |   | 4 |\n|   |   |   |   |\n+---+---+---+---+\n| 0\n1\n2\n3\n4 | 0\n1\n2\n\n\n3\n4 | 0\n1\n2\n3\n4\n\n\n | 0\n1\n2\n\n\n3\n4\n |\n+---+---+---+---+"
+);
+
 test_table!(
     iter_table_multiline_height_work,
     IterTable::new(
@@ -110,7 +161,7 @@ test_table!(
         ]
     )
     .height(3)
-    .to_string(),
+    ,
     "+---+---+---+---+"
     "| 0 | 1 | 2 | 3 |"
     "|   |   |   |   |"
@@ -132,7 +183,7 @@ test_table!(
         ]
     )
     .sniff(2)
-    .to_string(),
+    ,
     "+----+----+----+----+"
     "| 12 | 12 | 22 | 32 |"
     "+----+----+----+----+"
@@ -152,7 +203,7 @@ test_table!(
         ]
     )
     .sniff(2)
-    .to_string(),
+    ,
     "+-----+-----+-----+-----+"
     "| 023 | 123 | 223 | 323 |"
     "+-----+-----+-----+-----+"