Repository: zhiburt/tabled
Pull request #108 (2022-03-31 20:52:11)

Fix TotalWidth logic with spans


Patch:
diff --git a/papergrid/src/lib.rs b/papergrid/src/lib.rs
index 1c9944c..d0c2f72 100644
--- a/papergrid/src/lib.rs
+++ b/papergrid/src/lib.rs
@@ -47,6 +47,7 @@ const DEFAULT_SPLIT_INTERSECTION_CHAR: char = ' ';
 const DEFAULT_INDENT_FILL_CHAR: char = ' ';
 
 /// Grid provides a set of methods for building a text-based table
+#[derive(Debug, Clone)]
 pub struct Grid {
     size: (usize, usize),
     cells: Vec<Vec<String>>,
@@ -152,6 +153,10 @@ impl Grid {
         self.margin = margin
     }
 
+    pub fn get_margin(&self) -> &Margin {
+        &self.margin
+    }
+
     pub fn add_horizontal_split(&mut self, row: usize) {
         self.insert_horizontal_split(
             row,
@@ -299,7 +304,7 @@ impl Grid {
             .border(border)
     }
 
-    pub fn get_border(&mut self, row: usize, column: usize) -> Border {
+    pub fn get_border(&self, row: usize, column: usize) -> Border {
         self.borders.get_border(row, column).unwrap()
     }
 
@@ -472,28 +477,24 @@ impl Grid {
         self.override_split_lines.insert(row, line.into());
     }
 
-    pub fn row_width(&self, row: usize) -> usize {
-        let row_widths = (0..self.count_columns())
-            .map(|col| {
-                let style = self.style(&Entity::Cell(row, col));
-                let content = self.get_cell_content(row, col);
-                string_width(content) + style.padding.left.size + style.padding.right.size
-            })
-            .collect::<Vec<_>>();
+    // hide it by feature?
+    // 'private'
+    pub fn build_widths(&self) -> (Vec<Vec<usize>>, Vec<Vec<Style>>) {
+        let count_rows = self.count_rows();
+        let count_columns = self.count_columns();
 
-        let row_styles = (0..self.count_columns())
-            .map(|col| self.style(&Entity::Cell(row, col)).clone())
+        let mut cells = self.collect_cells(count_rows, count_columns);
+        let mut styles = self.collect_styles(count_rows, count_columns);
+
+        fix_spans(&mut styles, &mut cells);
+
+        let borders = (0..count_rows)
+            .map(|row| self.get_inner_split_line(row))
             .collect::<Vec<_>>();
 
-        let row_borders = self.get_inner_split_line(row);
+        let widths = columns_width(&cells, &styles, &borders, count_rows, count_columns);
 
-        row_width(
-            &row_styles,
-            &row_widths,
-            &row_borders,
-            0,
-            self.count_columns(),
-        )
+        (widths, styles)
     }
 
     fn add_split_lines(&mut self, entity: Entity, border: &Border) {
@@ -1902,7 +1903,7 @@ fn total_width(
     content_width + count_borders + margin.left.size + margin.right.size
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 struct Borders {
     vertical: HashMap<CellIndex, Line>,
     horizontal: HashMap<CellIndex, Line>,
diff --git a/src/width.rs b/src/width.rs
index 8c0e3d9..60ee3e3 100644
--- a/src/width.rs
+++ b/src/width.rs
@@ -19,7 +19,7 @@
 use std::collections::{HashMap, HashSet};
 
 use crate::{CellOption, TableOption};
-use papergrid::{string_width, Entity, Grid, Settings};
+use papergrid::{string_width, Entity, Grid, Settings, Style};
 
 /// MaxWidth allows you to set a max width of an object on a [Grid],
 /// using different strategies.
@@ -221,48 +221,63 @@ fn split_keeping_words(s: &str, width: usize) -> String {
     let mut buf = String::new();
     let mut i = 0;
     for c in s.chars() {
-        if i != 0 && i % width == 0 {
-            let prev_c = buf.chars().last().unwrap();
-            let is_splitting_word = !prev_c.is_whitespace() && !c.is_whitespace();
-            if is_splitting_word {
-                let pos = buf.chars().rev().position(|c| c.is_whitespace());
-                match pos {
-                    Some(pos) => {
-                        if pos < width {
-                            // it's a part of a word which we is ok to move to the next line;
-                            // we know that there will be enough space for this part + next character.
-                            //
-                            // todo: test about this next char space
-                            let range_len = buf
-                                .chars()
-                                .rev()
-                                .take(pos)
-                                .map(|c| c.len_utf8())
-                                .sum::<usize>();
-                            buf.insert(buf.len() - range_len, '\n');
-                            i = range_len;
-                        } else {
-                            // The words is too long to be moved,
-                            // we can't move it any way so just leave everything as it is
-                            buf.push('\n');
-                        }
-                    }
-                    None => {
-                        // We don't find a whitespace
-                        // so its a long word so we can do nothing about it
-                        buf.push('\n');
+        let is_splitting_pos = i == width;
+        if !is_splitting_pos {
+            i += 1;
+            buf.push(c);
+            continue;
+        }
+
+        i = 1;
+
+        let prev_c = buf.chars().last().unwrap();
+        let is_splitting_word = !prev_c.is_whitespace() && !c.is_whitespace();
+        if !is_splitting_word {
+            // This place doesn't separate a word
+            // So we just do a general split.
+            buf.push('\n');
+            buf.push(c);
+            continue;
+        }
+
+        let pos = buf.chars().rev().position(|c| c.is_whitespace());
+        match pos {
+            Some(pos) => {
+                if pos < width {
+                    // it's a part of a word which we is ok to move to the next line;
+                    // we know that there will be enough space for this part + next character.
+                    //
+                    // todo: test about this next char space
+                    let range_len = buf
+                        .chars()
+                        .rev()
+                        .take(pos)
+                        .map(|c| c.len_utf8())
+                        .sum::<usize>();
+
+                    // put an spaces in order to not limit widths and keep it correct.
+                    for i in 0..range_len {
+                        buf.insert(buf.len() - range_len - i, ' ');
                     }
+
+                    buf.insert(buf.len() - range_len, '\n');
+
+                    i = range_len + 1;
+                } else {
+                    // The words is too long to be moved,
+                    // we can't move it any way so just leave everything as it is
+                    buf.push('\n');
                 }
-            } else {
-                // This place doesn't separate a word
-                // So we just do a general split.
+
+                buf.push(c);
+            }
+            None => {
+                // We don't find a whitespace
+                // so its a long word so we can do nothing about it
                 buf.push('\n');
+                buf.push(c);
             }
         }
-
-        buf.push(c);
-
-        i += 1;
     }
 
     buf
@@ -426,7 +441,7 @@ fn increase_width(s: &str, width: usize, fill_with: char) -> String {
         s.lines()
             .map(|line| {
                 let length = string_width(line);
-                if length < width {
+                if width > length {
                     let remain = width - length;
                     let mut new_line = String::with_capacity(width);
                     new_line.push_str(line);
@@ -436,7 +451,8 @@ fn increase_width(s: &str, width: usize, fill_with: char) -> String {
                     std::borrow::Cow::Borrowed(line)
                 }
             })
-            .collect::<String>()
+            .collect::<Vec<_>>()
+            .join("\n")
     }
     #[cfg(feature = "color")]
     {
@@ -451,7 +467,8 @@ fn increase_width(s: &str, width: usize, fill_with: char) -> String {
                     line
                 }
             })
-            .collect::<String>()
+            .collect::<Vec<_>>()
+            .join("\n")
     }
 }
 
@@ -470,7 +487,7 @@ where
         }
 
         if self.width < total_width {
-            decrease_total_width(grid, self.width, false, false);
+            truncate_total_width(grid, self.width);
         }
     }
 }
@@ -487,7 +504,7 @@ impl TableOption for Wrap {
         }
 
         if self.width < total_width {
-            decrease_total_width(grid, self.width, true, self.keep_words);
+            wrap_total_width(grid, self.width, self.keep_words);
         }
     }
 }
@@ -504,14 +521,15 @@ impl TableOption for MinWidth {
         }
 
         if self.size > total_width {
-            increase_total_width(grid, self.size);
+            increase_total_width(grid, total_width, self.size);
         }
     }
 }
 
-fn increase_total_width(grid: &mut Grid, expected_width: usize) {
+fn increase_total_width(grid: &mut Grid, total_width: usize, expected_width: usize) {
+    let (_, styles) = grid.build_widths();
+
     let mut increase_list = HashMap::new();
-    let total_width = grid.total_width();
     let mut size = expected_width;
     for col in (0..grid.count_columns()).cycle() {
         if size == total_width {
@@ -519,8 +537,9 @@ fn increase_total_width(grid: &mut Grid, expected_width: usize) {
         }
 
         let mut increased = false;
+        #[allow(clippy::needless_range_loop)]
         for row in 0..grid.count_rows() {
-            let style = grid.style(&Entity::Cell(row, col));
+            let style = &styles[row][col];
             if style.span == 0 {
                 continue;
             }
@@ -541,81 +560,227 @@ fn increase_total_width(grid: &mut Grid, expected_width: usize) {
     for ((row, col), inc) in increase_list {
         let content = grid.get_cell_content(row, col);
         let content_width = string_width(content);
+
         MinWidth::new(content_width + inc).change_cell(grid, row, col);
     }
 }
 
-fn decrease_total_width(
-    grid: &mut Grid,
-    expected_width: usize,
-    wrap: bool,
-    wrap_keeping_words: bool,
-) -> bool {
-    let contents = build_contents(grid);
-    let mut widths = build_widths(grid);
-    let mut changes = HashSet::new();
-
-    while expected_width != grid.total_width() {
-        let row =
-            find_biggest_row(grid).expect("must never happen because we checked the length before");
-        let col = find_biggest_cell(&widths[row])
-            .expect("must never happen because we checked the length before");
-        let width = widths[row][col];
+fn truncate_total_width(grid: &mut Grid, width: usize) {
+    let points = decrease_total_width(grid, width);
 
-        if width == 0 {
-            // we checkend each cell and the biggest is 0
-            // so we can't do anything more in case of decrease
-            return false;
-        }
+    for ((row, col), width) in points {
+        Truncate::new(width).change_cell(grid, row, col);
+    }
+}
 
-        Truncate::new(width - 1).change_cell(grid, row, col);
+fn wrap_total_width(grid: &mut Grid, width: usize, keep_words: bool) {
+    let points = decrease_total_width(grid, width);
 
-        widths[row][col] -= 1;
-        changes.insert((row, col));
+    let mut wrap = Wrap::new(0);
+    wrap.keep_words = keep_words;
+    for ((row, col), width) in points {
+        wrap.width = width;
+        wrap.change_cell(grid, row, col);
     }
+}
+
+fn decrease_total_width(grid: &Grid, width: usize) -> HashMap<(usize, usize), usize> {
+    let mut points = HashMap::new();
+
+    let count_columns = grid.count_columns();
+    let count_rows = grid.count_rows();
 
-    if !wrap {
-        return true;
+    if count_columns == 0 || count_rows == 0 {
+        return points;
     }
 
-    set_contents(grid, contents);
+    let orig_widths = build_orig_widths(grid);
+
+    let mut min_widths = build_min_widths(grid);
+
+    let (mut widths, styles) = grid.build_widths();
+
+    correct_widths(&mut widths, &styles, count_rows, count_columns);
+    correct_widths(&mut min_widths, &styles, count_rows, count_columns);
+
+    let mut borders = build_borders_list(grid, &styles, count_rows, count_columns);
+    add_margin_width(grid, &mut borders);
+
+    let mut total_width =
+        new_total_width(&widths, &styles, &borders, count_rows, count_columns).unwrap_or(0);
+
+    let mut empty_columns = HashSet::new();
+    let mut columns = (0..count_columns).cycle();
+    while total_width != width {
+        let is_zeroed_table = empty_columns.len() == count_columns;
+        if is_zeroed_table {
+            break;
+        }
+
+        let col = columns.next().unwrap();
 
-    for (row, col) in changes {
-        let width = widths[row][col];
+        if empty_columns.contains(&col) {
+            continue;
+        }
 
-        let mut wrap = Wrap::new(width);
-        if wrap_keeping_words {
-            wrap = wrap.keep_words();
+        let is_empty_column = (0..count_rows).all(|row| widths[row][col] == 0);
+        if is_empty_column {
+            empty_columns.insert(col);
+            continue;
         }
 
-        wrap.change_cell(grid, row, col)
+        update_widths_column(&mut widths, &orig_widths, &styles, count_rows, col);
+
+        total_width =
+            new_total_width(&widths, &styles, &borders, count_rows, count_columns).unwrap_or(0);
+    }
+
+    for col in 0..count_columns {
+        for row in 0..count_rows {
+            let width = std::cmp::max(widths[row][col], min_widths[row][col]);
+            let orig_width = orig_widths[row][col];
+
+            if width < orig_width {
+                points.insert((row, col), width);
+            }
+        }
     }
 
-    true
+    points
 }
 
-fn build_contents(grid: &Grid) -> Vec<Vec<String>> {
-    (0..grid.count_rows())
+fn new_total_width(
+    widths: &[Vec<usize>],
+    styles: &[Vec<Style>],
+    count_borders: &[usize],
+    count_rows: usize,
+    count_columns: usize,
+) -> Option<usize> {
+    (0..count_rows)
         .map(|row| {
-            (0..grid.count_columns())
+            (0..count_columns)
+                .filter(|&col| styles[row][col].span > 0)
                 .map(|col| {
-                    let content = grid.get_cell_content(row, col);
-                    content.to_string()
+                    widths[row][col]
+                        + styles[row][col].padding.left.size
+                        + styles[row][col].padding.right.size
                 })
-                .collect()
+                .sum::<usize>()
+                + count_borders[row]
         })
-        .collect()
+        .max()
 }
 
-fn set_contents(grid: &mut Grid, contents: Vec<Vec<String>>) {
-    contents.into_iter().enumerate().for_each(|(row, rows)| {
-        rows.into_iter().enumerate().for_each(|(col, content)| {
-            grid.set(&Entity::Cell(row, col), Settings::default().text(content));
-        })
-    })
+fn build_borders_list(
+    grid: &Grid,
+    styles: &[Vec<Style>],
+    count_rows: usize,
+    count_columns: usize,
+) -> Vec<usize> {
+    let mut borders_count = Vec::with_capacity(count_rows);
+
+    #[allow(clippy::needless_range_loop)]
+    for row in 0..count_rows {
+        let mut count = 0;
+        for col in 0..count_columns {
+            if styles[row][col].span == 0 {
+                continue;
+            }
+
+            let border = grid.get_border(row, col);
+            if border.left.is_some() {
+                count += 1;
+            }
+
+            if col + 1 == count_columns && border.right.is_some() {
+                count += 1;
+            }
+        }
+
+        borders_count.push(count);
+    }
+
+    borders_count
+}
+
+fn add_margin_width(grid: &Grid, widths: &mut [usize]) {
+    let margin = grid.get_margin();
+
+    for w in widths {
+        *w += margin.left.size + margin.right.size;
+    }
+}
+
+fn correct_widths(
+    widths: &mut [Vec<usize>],
+    styles: &[Vec<Style>],
+    count_rows: usize,
+    count_columns: usize,
+) {
+    (0..count_rows).for_each(|row| {
+        (0..count_columns)
+            .for_each(|col| widths[row][col] = correct_width(&styles[row][col], widths[row][col]))
+    });
+}
+
+fn build_min_widths(grid: &Grid) -> Vec<Vec<usize>> {
+    let mut grid = grid.clone();
+    grid.set(&Entity::Global, Settings::default().text(""));
+
+    grid.build_widths().0
 }
 
-fn build_widths(grid: &Grid) -> Vec<Vec<usize>> {
+fn update_widths_column(
+    widths: &mut [Vec<usize>],
+    orig_widths: &[Vec<usize>],
+    styles: &[Vec<Style>],
+    count_rows: usize,
+    col: usize,
+) -> bool {
+    let mut some_content_was_changed = false;
+    for row in 0..count_rows {
+        let mut col = col;
+        while styles[row][col].span == 0 {
+            // todo:
+            // well it may happen, in a grid with all cells being with span 0.
+            // not sure how to handle it in a good way.
+            if col == 0 {
+                break;
+            }
+
+            col -= 1;
+        }
+
+        if widths[row][col] == 0 {
+            continue;
+        }
+
+        widths[row][col] -= 1;
+
+        let orig_content_was_changed = orig_widths[row][col] > widths[row][col];
+        if orig_content_was_changed {
+            some_content_was_changed = true;
+        }
+    }
+
+    some_content_was_changed
+}
+
+fn correct_width(style: &Style, mut width: usize) -> usize {
+    let mut padding = style.padding.left.size + style.padding.right.size;
+    while padding != 0 {
+        if width == 0 {
+            break;
+        }
+
+        width -= 1;
+        padding -= 1;
+    }
+
+    width
+}
+
+fn build_orig_widths(grid: &Grid) -> Vec<Vec<usize>> {
     (0..grid.count_rows())
         .map(|row| {
             (0..grid.count_columns())
@@ -627,11 +792,3 @@ fn build_widths(grid: &Grid) -> Vec<Vec<usize>> {
         })
         .collect()
 }
-
-fn find_biggest_row(grid: &Grid) -> Option<usize> {
-    (0..grid.count_rows()).max_by_key(|&row| grid.row_width(row))
-}
-
-fn find_biggest_cell(widths: &[usize]) -> Option<usize> {
-    (0..widths.len()).max_by_key(|&col| widths[col])
-}


Test patch:
diff --git a/tests/margin_test.rs b/tests/margin_test.rs
index 18afe6d..4ed7168 100644
--- a/tests/margin_test.rs
+++ b/tests/margin_test.rs
@@ -107,14 +107,13 @@ fn table_with_margin_and_min_width() {
         .with(MaxWidth::truncating(20))
         .to_string();
 
-    // todo: Seens like incorrect a bit
     let expected = concat!(
         "VVVVVVVVVVVVVVVVVVVV\n",
-        "> N | co | c |  c  <\n",
-        ">---+----+---+-----<\n",
-        "> 0 |  0-0   | 0-2 <\n",
-        "> 1 | 1- | 1 |  1  <\n",
-        "> 2 | 2- | 2 |  2  <\n",
+        ">  | co | co | col <\n",
+        ">--+----+----+-----<\n",
+        ">  |   0-0   | 0-2 <\n",
+        ">  | 1- | 1- | 1-2 <\n",
+        ">  | 2- | 2- | 2-2 <\n",
         "^^^^^^^^^^^^^^^^^^^^\n",
     );
 
diff --git a/tests/width_test.rs b/tests/width_test.rs
index 8a160be..d4ac1ff 100644
--- a/tests/width_test.rs
+++ b/tests/width_test.rs
@@ -1,7 +1,7 @@
 use crate::util::create_vector;
 use tabled::{
     object::{Cell, Columns, Full, Object, Rows},
-    Alignment, MaxWidth, MinWidth, Modify, Panel, Style, Table,
+    Alignment, MaxWidth, MinWidth, Modify, Panel, Span, Style, Table, Tabled,
 };
 
 mod util;
@@ -116,14 +116,16 @@ fn max_width_wrapped_keep_words() {
         .with(Modify::new(Full).with(MaxWidth::wrapping(17).keep_words()))
         .to_string();
 
-    let expected = concat!(
-        "| &str            |\n",
-        "|-----------------|\n",
-        "| this is a long  |\n",
-        "| sentence        |\n",
+    assert_eq!(
+        table,
+        concat!(
+            "| &str              |\n",
+            "|-------------------|\n",
+            "| this is a long    |\n",
+            "| sentence          |\n",
+        )
     );
-
-    assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 17 + 2 + 2);
 
     let data = vec!["this is a long  sentence"];
     let table = Table::new(&data)
@@ -132,14 +134,16 @@ fn max_width_wrapped_keep_words() {
         .with(Modify::new(Full).with(MaxWidth::wrapping(17).keep_words()))
         .to_string();
 
-    let expected = concat!(
-        "| &str             |\n",
-        "|------------------|\n",
-        "| this is a long   |\n",
-        "| sentence         |\n",
+    assert_eq!(
+        table,
+        concat!(
+            "| &str              |\n",
+            "|-------------------|\n",
+            "| this is a long    |\n",
+            "| sentence          |\n",
+        )
     );
-
-    assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 17 + 2 + 2);
 
     let data = vec!["this is a long   sentence"];
     let table = Table::new(&data)
@@ -645,14 +649,15 @@ fn total_width_small() {
         .to_string();
 
     let expected = concat!(
-        "| N |  |  |  |\n",
-        "|---+--+--+--|\n",
-        "| 0 |  |  |  |\n",
-        "| 1 |  |  |  |\n",
-        "| 2 |  |  |  |\n",
+        "|  |  |  | c |\n",
+        "|--+--+--+---|\n",
+        "|  |  |  | 0 |\n",
+        "|  |  |  | 1 |\n",
+        "|  |  |  | 2 |\n",
     );
 
     assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 14);
 }
 
 #[test]
@@ -678,6 +683,54 @@ fn total_width_smaller_then_content() {
 #[test]
 fn total_width_small_with_panel() {
     let data = create_vector::<3, 3>();
+
+    let table = Table::new(&data)
+        .with(Modify::new(Full).with(Alignment::center_horizontal()))
+        .with(Style::github_markdown())
+        .with(MaxWidth::truncating(20))
+        .with(MinWidth::new(20))
+        .to_string();
+
+    let expected = concat!(
+        "|  | co | co | col |\n",
+        "|--+----+----+-----|\n",
+        "|  | 0- | 0- | 0-2 |\n",
+        "|  | 1- | 1- | 1-2 |\n",
+        "|  | 2- | 2- | 2-2 |\n",
+    );
+
+    assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 20);
+
+    let table = Table::new(Vec::<usize>::new())
+        .with(Panel("Hello World", 0))
+        .with(MaxWidth::truncating(5))
+        .with(MinWidth::new(5))
+        .to_string();
+
+    let expected = concat!(" Hel \n", "+---+\n", "| u |\n", "+---+\n",);
+
+    assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 5);
+
+    let table = Table::new(&create_vector::<1, 2>())
+        .with(Panel("Hello World", 0))
+        .with(Modify::new(Full).with(Alignment::center_horizontal()))
+        .with(Style::github_markdown())
+        .with(MaxWidth::truncating(20))
+        .with(MinWidth::new(20))
+        .to_string();
+
+    let expected = concat!(
+        "|   Hello World    |\n",
+        "|--+-------+-------|\n",
+        "|  | colum | colum |\n",
+        "|  |  0-0  |  0-1  |\n",
+    );
+
+    assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 20);
+
     let table = Table::new(&data)
         .with(Panel("Hello World", 0))
         .with(Modify::new(Full).with(Alignment::center_horizontal()))
@@ -688,14 +741,126 @@ fn total_width_small_with_panel() {
 
     let expected = concat!(
         "|   Hello World    |\n",
-        "|---+----+----+----|\n",
-        "| N | co | co | co |\n",
-        "| 0 | 0- | 0- | 0- |\n",
-        "| 1 | 1- | 1- | 1- |\n",
-        "| 2 | 2- | 2- | 2- |\n",
+        "|--+----+----+-----|\n",
+        "|  | co | co | col |\n",
+        "|  | 0- | 0- | 0-2 |\n",
+        "|  | 1- | 1- | 1-2 |\n",
+        "|  | 2- | 2- | 2-2 |\n",
     );
 
     assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 20);
+
+    let table = Table::new(&data)
+        .with(Panel("Hello World", 0))
+        .with(Modify::new(Full).with(Alignment::center_horizontal()))
+        .with(Style::github_markdown())
+        .with(MaxWidth::truncating(6))
+        .with(MinWidth::new(6))
+        .to_string();
+
+    let expected = concat!(
+        "|Hello World|\n",
+        "|--+--+--+--|\n",
+        "|  |  |  |  |\n",
+        "|  |  |  |  |\n",
+        "|  |  |  |  |\n",
+        "|  |  |  |  |\n",
+    );
+
+    assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 13);
+
+    let table = Table::new(&data)
+        .with(Panel("Hello World", 0))
+        .with(Modify::new(Full).with(Alignment::center_horizontal()))
+        .with(Style::github_markdown())
+        .with(MaxWidth::truncating(14))
+        .with(MinWidth::new(14))
+        .to_string();
+
+    let expected = concat!(
+        "|Hello World |\n",
+        "|--+--+--+---|\n",
+        "|  |  |  | c |\n",
+        "|  |  |  | 0 |\n",
+        "|  |  |  | 1 |\n",
+        "|  |  |  | 2 |\n",
+    );
+
+    assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 14);
+
+    let table = Table::new(&data)
+        .with(Panel("Hello World 123", 0))
+        .with(Modify::new(Full).with(Alignment::center_horizontal()))
+        .with(Style::github_markdown())
+        .with(MaxWidth::truncating(14))
+        .with(MinWidth::new(14))
+        .to_string();
+
+    let expected = concat!(
+        "|Hello World |\n",
+        "|--+--+--+---|\n",
+        "|  |  |  | c |\n",
+        "|  |  |  | 0 |\n",
+        "|  |  |  | 1 |\n",
+        "|  |  |  | 2 |\n",
+    );
+
+    assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 14);
+}
+
+#[test]
+fn total_width_wrapping() {
+    let data = create_vector::<3, 3>();
+    let table = Table::new(&data)
+        .with(Modify::new(Full).with(Alignment::center_horizontal()))
+        .with(Style::github_markdown())
+        .with(MaxWidth::wrapping(20))
+        .with(MinWidth::new(20))
+        .to_string();
+
+    let expected = concat!(
+        "|  | co | co | col |\n",
+        "|  | lu | lu | umn |\n",
+        "|  | mn | mn |  2  |\n",
+        "|  |  0 |  1 |     |\n",
+        "|--+----+----+-----|\n",
+        "|  | 0- | 0- | 0-2 |\n",
+        "|  | 0  | 1  |     |\n",
+        "|  | 1- | 1- | 1-2 |\n",
+        "|  | 0  | 1  |     |\n",
+        "|  | 2- | 2- | 2-2 |\n",
+        "|  | 0  | 1  |     |\n",
+    );
+
+    assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 20);
+
+    let mut data = create_vector::<3, 3>();
+    data[2][2] = "some loong string".to_owned();
+    let table = Table::new(&data)
+        .with(Modify::new(Full).with(Alignment::center_horizontal()))
+        .with(Style::github_markdown())
+        .with(MaxWidth::wrapping(20).keep_words())
+        .with(MinWidth::new(20))
+        .to_string();
+
+    let expected = concat!(
+        "|  |  | column  |  |\n",
+        "|  |  | 1       |  |\n",
+        "|--+--+---------+--|\n",
+        "|  |  |   0-1   |  |\n",
+        "|  |  |   1-1   |  |\n",
+        "|  |  | some    |  |\n",
+        "|  |  | loong   |  |\n",
+        "|  |  | string  |  |\n",
+    );
+
+    assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 20);
 }
 
 #[test]
@@ -711,18 +876,211 @@ fn total_width_small_with_panel_using_wrapping() {
 
     let expected = concat!(
         "|   Hello World    |\n",
-        "|---+----+----+----|\n",
-        "| N | co | co | co |\n",
-        "|   | lu | lu | lu |\n",
-        "|   | mn | mn | mn |\n",
-        "|   |  0 |  1 |  2 |\n",
-        "| 0 | 0- | 0- | 0- |\n",
-        "|   | 0  | 1  | 2  |\n",
-        "| 1 | 1- | 1- | 1- |\n",
-        "|   | 0  | 1  | 2  |\n",
-        "| 2 | 2- | 2- | 2- |\n",
-        "|   | 0  | 1  | 2  |\n",
+        "|--+----+----+-----|\n",
+        "|  | co | co | col |\n",
+        "|  | lu | lu | umn |\n",
+        "|  | mn | mn |  2  |\n",
+        "|  |  0 |  1 |     |\n",
+        "|  | 0- | 0- | 0-2 |\n",
+        "|  | 0  | 1  |     |\n",
+        "|  | 1- | 1- | 1-2 |\n",
+        "|  | 0  | 1  |     |\n",
+        "|  | 2- | 2- | 2-2 |\n",
+        "|  | 0  | 1  |     |\n",
     );
 
     assert_eq!(table, expected);
+    assert_eq!(lines_widths(&table)[0], 20);
+
+    let table = Table::new(&data)
+        .with(Panel("Hello World", 0))
+        .with(Modify::new(Full).with(Alignment::center_horizontal()))
+        .with(Style::github_markdown())
+        .with(MaxWidth::wrapping(14))
+        .with(MinWidth::new(14))
+        .to_string();
+
+    assert_eq!(
+        table,
+        concat!(
+            "|Hello World |\n",
+            "|--+--+--+---|\n",
+            "|  |  |  | c |\n",
+            "|  |  |  | o |\n",
+            "|  |  |  | l |\n",
+            "|  |  |  | u |\n",
+            "|  |  |  | m |\n",
+            "|  |  |  | n |\n",
+            "|  |  |  |   |\n",
+            "|  |  |  | 2 |\n",
+            "|  |  |  | 0 |\n",
+            "|  |  |  | - |\n",
+            "|  |  |  | 2 |\n",
+            "|  |  |  | 1 |\n",
+            "|  |  |  | - |\n",
+            "|  |  |  | 2 |\n",
+            "|  |  |  | 2 |\n",
+            "|  |  |  | - |\n",
+            "|  |  |  | 2 |\n",
+        )
+    );
+    assert_eq!(lines_widths(&table)[0], 14);
+
+    let table = Table::new(&data)
+        .with(Panel("Hello World 123", 0))
+        .with(Modify::new(Full).with(Alignment::center_horizontal()))
+        .with(Style::github_markdown())
+        .with(MaxWidth::wrapping(14))
+        .with(MinWidth::new(14))
+        .to_string();
+
+    assert_eq!(
+        table,
+        concat!(
+            "|Hello World |\n",
+            "| 123        |\n",
+            "|--+--+--+---|\n",
+            "|  |  |  | c |\n",
+            "|  |  |  | o |\n",
+            "|  |  |  | l |\n",
+            "|  |  |  | u |\n",
+            "|  |  |  | m |\n",
+            "|  |  |  | n |\n",
+            "|  |  |  |   |\n",
+            "|  |  |  | 2 |\n",
+            "|  |  |  | 0 |\n",
+            "|  |  |  | - |\n",
+            "|  |  |  | 2 |\n",
+            "|  |  |  | 1 |\n",
+            "|  |  |  | - |\n",
+            "|  |  |  | 2 |\n",
+            "|  |  |  | 2 |\n",
+            "|  |  |  | - |\n",
+            "|  |  |  | 2 |\n",
+        )
+    );
+    assert_eq!(lines_widths(&table)[0], 14);
+}
+
+#[test]
+fn max_width_with_span() {
+    let mut data = create_vector::<3, 3>();
+    data[0][1] = "a long string".to_string();
+
+    let table = Table::new(&data)
+        .with(Style::psql())
+        .with(Modify::new(Cell(1, 1)).with(Span::column(2)))
+        .with(Modify::new(Cell(2, 2)).with(Span::column(2)));
+
+    let table = table.with(MaxWidth::truncating(40));
+
+    println!("{}", table);
+
+    assert_eq!(
+        table.to_string(),
+        concat!(
+            " N | column 0 | column 1 | column 2 \n",
+            "---+----------+----------+----------\n",
+            " 0 |    a long string    |   0-2    \n",
+            " 1 |   1-0    |         1-1         \n",
+            " 2 |   2-0    |   2-1    |   2-2    \n",
+        )
+    );
+
+    let table = table.with(MaxWidth::truncating(20));
+
+    println!("{}", table);
+
+    assert_eq!(
+        table.to_string(),
+        concat!(
+            "  | col | col | col \n",
+            "--+-----+-----+-----\n",
+            "  | a long st | 0-2 \n",
+            "  | 1-0 |    1-1    \n",
+            "  | 2-0 | 2-1 | 2-2 \n",
+        )
+    );
+
+    let table = table.with(MaxWidth::truncating(10));
+
+    println!("{}", table);
+
+    assert_eq!(
+        table.to_string(),
+        concat!(
+            "  |  |  |  \n",
+            "--+--+--+--\n",
+            "  | a l |  \n",
+            "  |  | 1-1 \n",
+            "  |  |  |  \n",
+        )
+    );
+}
+
+#[test]
+fn wrapping_as_total_multiline() {
+    #[derive(Tabled)]
+    struct D<'a>(
+        #[tabled(rename = "version")] &'a str,
+        #[tabled(rename = "published_date")] &'a str,
+        #[tabled(rename = "is_active")] &'a str,
+        #[tabled(rename = "major_feature")] &'a str,
+    );
+
+    let data = vec![
+        D("0.2.1", "2021-06-23", "true", "#[header(inline)] attribute"),
+        D("0.2.0", "2021-06-19", "false", "API changes"),
+        D("0.1.4", "2021-06-07", "false", "display_with attribute"),
+    ];
+
+    let table = Table::new(&data)
+        .with(Style::github_markdown())
+        .with(Modify::new(Full).with(Alignment::left()))
+        .with(MaxWidth::wrapping(57))
+        .to_string();
+
+    assert_eq!(
+        table,
+        concat!(
+            "| ver | published_d | is_act | major_feature            |\n",
+            "| sio | ate         | ive    |                          |\n",
+            "| n   |             |        |                          |\n",
+            "|-----+-------------+--------+--------------------------|\n",
+            "| 0.2 | 2021-06-23  | true   | #[header(inline)] attrib |\n",
+            "| .1  |             |        | ute                      |\n",
+            "| 0.2 | 2021-06-19  | false  | API changes              |\n",
+            "| .0  |             |        |                          |\n",
+            "| 0.1 | 2021-06-07  | false  | display_with attribute   |\n",
+            "| .4  |             |        |                          |\n",
+        )
+    );
+    assert_eq!(lines_widths(&table)[0], 57);
+
+    let table = Table::new(&data)
+        .with(Style::github_markdown())
+        .with(Modify::new(Full).with(Alignment::left()))
+        .with(MaxWidth::wrapping(57).keep_words())
+        .to_string();
+
+    assert_eq!(
+        table,
+        concat!(
+            "| ver | published_d | is_act | major_feature            |\n",
+            "| sio | ate         | ive    |                          |\n",
+            "| n   |             |        |                          |\n",
+            "|-----+-------------+--------+--------------------------|\n",
+            "| 0.2 | 2021-06-23  | true   | #[header(inline)]        |\n",
+            "| .1  |             |        | attribute                |\n",
+            "| 0.2 | 2021-06-19  | false  | API changes              |\n",
+            "| .0  |             |        |                          |\n",
+            "| 0.1 | 2021-06-07  | false  | display_with attribute   |\n",
+            "| .4  |             |        |                          |\n",
+        )
+    );
+    assert_eq!(lines_widths(&table)[0], 57);
+}
+
+fn lines_widths(s: &str) -> Vec<usize> {
+    s.lines().map(|l| l.chars().count()).collect()
 }