From 56d59c0181d011f05dc1009c867fa6ce7c39f434 Mon Sep 17 00:00:00 2001 From: Arne Beer Date: Fri, 6 Aug 2021 23:21:19 +0200 Subject: [PATCH] change: Handle multi-space utf-8 characters --- Cargo.toml | 7 ++++--- src/row.rs | 4 +++- src/utils/arrangement/dynamic.rs | 6 ++++-- src/utils/formatting/content_format.rs | 11 ++++++----- src/utils/formatting/content_split.rs | 10 ++++++---- tests/all/content_arrangement_test.rs | 5 +++-- tests/all/property_test.rs | 6 ++++-- 7 files changed, 30 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7056d68..1ecf06c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,10 @@ edition = "2018" maintenance = { status = "actively-developed" } [dependencies] -crossterm = "^0.20" -strum = "^0.21" -strum_macros = "^0.21" +crossterm = "0.20" +strum = "0.21" +strum_macros = "0.21" +unicode-width = "0.1" [dev-dependencies] pretty_assertions = "0.7" diff --git a/src/row.rs b/src/row.rs index 0e66d27..d178f21 100644 --- a/src/row.rs +++ b/src/row.rs @@ -1,5 +1,7 @@ use std::slice::Iter; +use unicode_width::UnicodeWidthStr; + use crate::cell::{Cell, Cells}; /// Each row contains [Cells](crate::Cell) and can be added to a [Table](crate::Table). @@ -65,7 +67,7 @@ impl Row { // Each entry represents the longest string width for a cell. cell.content .iter() - .map(|string| string.chars().count()) + .map(|string| string.width()) .max() .unwrap_or(0) }) diff --git a/src/utils/arrangement/dynamic.rs b/src/utils/arrangement/dynamic.rs index fa40415..6637fda 100644 --- a/src/utils/arrangement/dynamic.rs +++ b/src/utils/arrangement/dynamic.rs @@ -1,5 +1,7 @@ use std::convert::TryInto; +use unicode_width::UnicodeWidthStr; + use super::constraints::get_max_constraint; use super::constraints::get_min_constraint; use super::helper::*; @@ -423,7 +425,7 @@ fn get_longest_line_after_split(average_space: usize, column: &Column, table: &T // Iterate over each line and split it into multiple lines, if necessary. // Newlines added by the user will be preserved. for line in cell.content.iter() { - if (line.chars().count()) > average_space { + if line.width() > average_space { let mut splitted = split_line(line, &info, delimiter); column_lines.append(&mut splitted); } else { @@ -435,7 +437,7 @@ fn get_longest_line_after_split(average_space: usize, column: &Column, table: &T // Get the longest line, default to length 0 if no lines exist. column_lines .iter() - .map(|line| line.chars().count()) + .map(|line| line.width()) .max() .unwrap_or(0) } diff --git a/src/utils/formatting/content_format.rs b/src/utils/formatting/content_format.rs index 4baffbe..a0bea5d 100644 --- a/src/utils/formatting/content_format.rs +++ b/src/utils/formatting/content_format.rs @@ -1,4 +1,5 @@ use crossterm::style::{style, Stylize}; +use unicode_width::UnicodeWidthStr; use super::content_split::split_line; use crate::cell::Cell; @@ -85,7 +86,7 @@ pub fn format_row( // Iterate over each line and split it into multiple lines, if necessary. // Newlines added by the user will be preserved. for line in cell.content.iter() { - if line.chars().count() > info.content_width.into() { + if line.width() > info.content_width.into() { let mut splitted = split_line(line, info, delimiter); cell_lines.append(&mut splitted); } else { @@ -108,9 +109,9 @@ pub fn format_row( let width: usize = info.content_width.into(); if width >= 6 { // Truncate the line if '...' doesn't fit - if last_line.chars().count() >= width - 3 { - let surplus = (last_line.chars().count() + 3) - width; - last_line.truncate(last_line.chars().count() - surplus); + if last_line.width() >= width - 3 { + let surplus = (last_line.width() + 3) - width; + last_line.truncate(last_line.width() - surplus); } last_line.push_str("..."); } @@ -180,7 +181,7 @@ pub fn format_row( /// Padding is applied in this function as well. fn align_line(mut line: String, info: &ColumnDisplayInfo, cell: &Cell) -> String { let content_width = info.content_width; - let remaining: usize = usize::from(content_width).saturating_sub(line.chars().count()); + let remaining: usize = usize::from(content_width).saturating_sub(line.width()); // Determine the alignment of the column cells. // Cell settings overwrite the columns Alignment settings. diff --git a/src/utils/formatting/content_split.rs b/src/utils/formatting/content_split.rs index 6dbee0a..c1e7428 100644 --- a/src/utils/formatting/content_split.rs +++ b/src/utils/formatting/content_split.rs @@ -1,5 +1,7 @@ use std::iter::FromIterator; +use unicode_width::UnicodeWidthStr; + use crate::utils::ColumnDisplayInfo; /// Split a line if it's longer than the allowed columns (width - padding). @@ -28,8 +30,8 @@ pub fn split_line(line: &str, info: &ColumnDisplayInfo, delimiter: char) -> Vec< let mut current_line = String::new(); while let Some(next) = elements.pop() { - let current_length = current_line.chars().count(); - let next_length = next.chars().count(); + let current_length = current_line.width(); + let next_length = next.width(); // Some helper variables // The length of the current line when combining it with the next element @@ -39,7 +41,7 @@ pub fn split_line(line: &str, info: &ColumnDisplayInfo, delimiter: char) -> Vec< added_length += 1; } // The remaining width for this column. If we are on a non-empty line, subtract 1 for the delimiter. - let mut remaining_width = content_width - current_line.chars().count(); + let mut remaining_width = content_width - current_line.width(); if !current_line.is_empty() { remaining_width = remaining_width.saturating_sub(1); } @@ -128,7 +130,7 @@ const MIN_FREE_CHARS: usize = 2; /// Otherwise, we simply return the current line and basically don't do anything. fn check_if_full(lines: &mut Vec, content_width: usize, current_line: String) -> String { // Already complete the current line, if there isn't space for more than two chars - if current_line.chars().count() > content_width.saturating_sub(MIN_FREE_CHARS) { + if current_line.width() > content_width.saturating_sub(MIN_FREE_CHARS) { lines.push(current_line); return String::new(); } diff --git a/tests/all/content_arrangement_test.rs b/tests/all/content_arrangement_test.rs index 6556ab4..2ea9a58 100644 --- a/tests/all/content_arrangement_test.rs +++ b/tests/all/content_arrangement_test.rs @@ -3,10 +3,11 @@ use pretty_assertions::assert_eq; use comfy_table::ColumnConstraint::*; use comfy_table::Width::*; use comfy_table::{ContentArrangement, Row, Table}; +use unicode_width::UnicodeWidthStr; fn assert_table_lines(table: &Table, count: usize) { for line in table.lines() { - assert_eq!(line.chars().count(), count); + assert_eq!(line.width(), count); } } @@ -66,7 +67,7 @@ fn simple_dynamic_table() { | | stuff | | +--------+-------+------+"; println!("{}", expected); - assert!(table.lines().all(|line| line.chars().count() == 25)); + assert!(table.lines().all(|line| line.width() == 25)); assert_eq!("\n".to_string() + &table.to_string(), expected); } diff --git a/tests/all/property_test.rs b/tests/all/property_test.rs index 29508c8..6d8ef3a 100644 --- a/tests/all/property_test.rs +++ b/tests/all/property_test.rs @@ -4,6 +4,7 @@ use comfy_table::presets::UTF8_FULL; use comfy_table::ColumnConstraint::*; use comfy_table::Width::*; use comfy_table::*; +use unicode_width::UnicodeWidthStr; /// Pick any of the three existing ContentArrangement types for the table. fn content_arrangement() -> impl Strategy { @@ -162,13 +163,14 @@ proptest! { let mut line_iter = lines.iter(); let line_length = if let Some(line) = line_iter.next() { - line.chars().count() + line.width() } else { 0 }; for line in line_iter { - if line.chars().count() != line_length { + if line.width() != line_length { + println!("{}", formatted); return Err(TestCaseError::Fail("Each line of a printed table has to have the same length!".into())) } }