diff options
Diffstat (limited to 'vendor/tabled/src/settings/width')
-rw-r--r-- | vendor/tabled/src/settings/width/justify.rs | 96 | ||||
-rw-r--r-- | vendor/tabled/src/settings/width/min_width.rs | 205 | ||||
-rw-r--r-- | vendor/tabled/src/settings/width/mod.rs | 163 | ||||
-rw-r--r-- | vendor/tabled/src/settings/width/truncate.rs | 505 | ||||
-rw-r--r-- | vendor/tabled/src/settings/width/util.rs | 265 | ||||
-rw-r--r-- | vendor/tabled/src/settings/width/width_list.rs | 50 | ||||
-rw-r--r-- | vendor/tabled/src/settings/width/wrap.rs | 1468 |
7 files changed, 2752 insertions, 0 deletions
diff --git a/vendor/tabled/src/settings/width/justify.rs b/vendor/tabled/src/settings/width/justify.rs new file mode 100644 index 000000000..03b2afe0d --- /dev/null +++ b/vendor/tabled/src/settings/width/justify.rs @@ -0,0 +1,96 @@ +//! This module contains [`Justify`] structure, used to set an exact width to each column. + +use crate::{ + grid::config::ColoredConfig, + grid::records::{ExactRecords, PeekableRecords, Records, RecordsMut}, + settings::{ + measurement::{Max, Measurement, Min}, + CellOption, TableOption, Width, + }, +}; + +/// Justify sets all columns widths to the set value. +/// +/// Be aware that it doesn't consider padding. +/// So if you want to set a exact width you might need to use [`Padding`] to set it to 0. +/// +/// ## Examples +/// +/// ``` +/// use tabled::{Table, settings::{Width, Style, object::Segment, Padding, Modify}}; +/// +/// let data = ["Hello", "World", "!"]; +/// +/// let table = Table::new(&data) +/// .with(Style::markdown()) +/// .with(Modify::new(Segment::all()).with(Padding::zero())) +/// .with(Width::justify(3)); +/// ``` +/// +/// [`Max`] usage to justify by a max column width. +/// +/// ``` +/// use tabled::{Table, settings::{width::Justify, Style}}; +/// +/// let data = ["Hello", "World", "!"]; +/// +/// let table = Table::new(&data) +/// .with(Style::markdown()) +/// .with(Justify::max()); +/// ``` +/// +/// [`Padding`]: crate::settings::Padding +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Justify<W> { + width: W, +} + +impl<W> Justify<W> +where + W: Measurement<Width>, +{ + /// Creates a new [`Justify`] instance. + /// + /// Be aware that [`Padding`] is not considered when comparing the width. + /// + /// [`Padding`]: crate::settings::Padding + pub fn new(width: W) -> Self { + Self { width } + } +} + +impl Justify<Max> { + /// Creates a new Justify instance with a Max width used as a value. + pub fn max() -> Self { + Self { width: Max } + } +} + +impl Justify<Min> { + /// Creates a new Justify instance with a Min width used as a value. + pub fn min() -> Self { + Self { width: Min } + } +} + +impl<R, D, W> TableOption<R, D, ColoredConfig> for Justify<W> +where + W: Measurement<Width>, + R: Records + ExactRecords + PeekableRecords + RecordsMut<String>, + for<'a> &'a R: Records, +{ + fn change(self, records: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let width = self.width.measure(&*records, cfg); + + let count_rows = records.count_rows(); + let count_columns = records.count_columns(); + + for row in 0..count_rows { + for col in 0..count_columns { + let pos = (row, col).into(); + CellOption::change(Width::increase(width), records, cfg, pos); + CellOption::change(Width::truncate(width), records, cfg, pos); + } + } + } +} diff --git a/vendor/tabled/src/settings/width/min_width.rs b/vendor/tabled/src/settings/width/min_width.rs new file mode 100644 index 000000000..afb9ae302 --- /dev/null +++ b/vendor/tabled/src/settings/width/min_width.rs @@ -0,0 +1,205 @@ +//! This module contains [`MinWidth`] structure, used to increase width of a [`Table`]s or a cell on a [`Table`]. +//! +//! [`Table`]: crate::Table + +use std::marker::PhantomData; + +use crate::{ + grid::config::ColoredConfig, + grid::config::Entity, + grid::dimension::CompleteDimensionVecRecords, + grid::records::{ExactRecords, PeekableRecords, Records, RecordsMut}, + grid::util::string::{get_lines, string_width_multiline}, + settings::{ + measurement::Measurement, + peaker::{Peaker, PriorityNone}, + CellOption, TableOption, Width, + }, +}; + +use super::util::get_table_widths_with_total; + +/// [`MinWidth`] changes a content in case if it's length is lower then the boundary. +/// +/// It can be applied to a whole table. +/// +/// It does nothing in case if the content's length is bigger then the boundary. +/// +/// Be aware that further changes of the table may cause the width being not set. +/// For example applying [`Padding`] after applying [`MinWidth`] will make the former have no affect. +/// (You should use [`Padding`] first). +/// +/// Be aware that it doesn't consider padding. +/// So if you want to set a exact width you might need to use [`Padding`] to set it to 0. +/// +/// ## Examples +/// +/// Cell change +/// +/// ``` +/// use tabled::{Table, settings::{object::Segment, Width, Style, Modify}}; +/// +/// let data = ["Hello", "World", "!"]; +/// +/// let table = Table::new(&data) +/// .with(Style::markdown()) +/// .with(Modify::new(Segment::all()).with(Width::increase(10))); +/// ``` +/// Table change +/// +/// ``` +/// use tabled::{Table, settings::Width}; +/// +/// let table = Table::new(&["Hello World!"]).with(Width::increase(5)); +/// ``` +/// +/// [`Padding`]: crate::settings::Padding +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct MinWidth<W = usize, P = PriorityNone> { + width: W, + fill: char, + _priority: PhantomData<P>, +} + +impl<W> MinWidth<W> +where + W: Measurement<Width>, +{ + /// Creates a new instance of [`MinWidth`]. + pub fn new(width: W) -> Self { + Self { + width, + fill: ' ', + _priority: PhantomData, + } + } +} + +impl<W, P> MinWidth<W, P> { + /// Set's a fill character which will be used to fill the space + /// when increasing the length of the string to the set boundary. + /// + /// Used only if chaning cells. + pub fn fill_with(mut self, c: char) -> Self { + self.fill = c; + self + } + + /// Priority defines the logic by which a increase of width will be applied when is done for the whole table. + /// + /// - [`PriorityNone`] which inc the columns one after another. + /// - [`PriorityMax`] inc the biggest columns first. + /// - [`PriorityMin`] inc the lowest columns first. + /// + /// [`PriorityMax`]: crate::settings::peaker::PriorityMax + /// [`PriorityMin`]: crate::settings::peaker::PriorityMin + pub fn priority<PP: Peaker>(self) -> MinWidth<W, PP> { + MinWidth { + fill: self.fill, + width: self.width, + _priority: PhantomData, + } + } +} + +impl<W, R> CellOption<R, ColoredConfig> for MinWidth<W> +where + W: Measurement<Width>, + R: Records + ExactRecords + PeekableRecords + RecordsMut<String>, + for<'a> &'a R: Records, +{ + fn change(self, records: &mut R, cfg: &mut ColoredConfig, entity: Entity) { + let width = self.width.measure(&*records, cfg); + + let count_rows = records.count_rows(); + let count_columns = records.count_columns(); + + for pos in entity.iter(count_rows, count_columns) { + let is_valid_pos = pos.0 < count_rows && pos.1 < count_columns; + if !is_valid_pos { + continue; + } + + let cell = records.get_text(pos); + let cell_width = string_width_multiline(cell); + if cell_width >= width { + continue; + } + + let content = increase_width(cell, width, self.fill); + records.set(pos, content); + } + } +} + +impl<W, P, R> TableOption<R, CompleteDimensionVecRecords<'static>, ColoredConfig> for MinWidth<W, P> +where + W: Measurement<Width>, + P: Peaker, + R: Records + ExactRecords + PeekableRecords, + for<'a> &'a R: Records, +{ + fn change( + self, + records: &mut R, + cfg: &mut ColoredConfig, + dims: &mut CompleteDimensionVecRecords<'static>, + ) { + if records.count_rows() == 0 || records.count_columns() == 0 { + return; + } + + let nessary_width = self.width.measure(&*records, cfg); + + let (widths, total_width) = get_table_widths_with_total(&*records, cfg); + if total_width >= nessary_width { + return; + } + + let widths = get_increase_list(widths, nessary_width, total_width, P::create()); + let _ = dims.set_widths(widths); + } +} + +fn get_increase_list<F>( + mut widths: Vec<usize>, + need: usize, + mut current: usize, + mut peaker: F, +) -> Vec<usize> +where + F: Peaker, +{ + while need != current { + let col = match peaker.peak(&[], &widths) { + Some(col) => col, + None => break, + }; + + widths[col] += 1; + current += 1; + } + + widths +} + +fn increase_width(s: &str, width: usize, fill_with: char) -> String { + use crate::grid::util::string::string_width; + use std::{borrow::Cow, iter::repeat}; + + get_lines(s) + .map(|line| { + let length = string_width(&line); + + if length < width { + let mut line = line.into_owned(); + let remain = width - length; + line.extend(repeat(fill_with).take(remain)); + Cow::Owned(line) + } else { + line + } + }) + .collect::<Vec<_>>() + .join("\n") +} diff --git a/vendor/tabled/src/settings/width/mod.rs b/vendor/tabled/src/settings/width/mod.rs new file mode 100644 index 000000000..c1202f70f --- /dev/null +++ b/vendor/tabled/src/settings/width/mod.rs @@ -0,0 +1,163 @@ +//! This module contains object which can be used to limit a cell to a given width: +//! +//! - [`Truncate`] cuts a cell content to limit width. +//! - [`Wrap`] split the content via new lines in order to fit max width. +//! - [`Justify`] sets columns width to the same value. +//! +//! To set a a table width, a combination of [`Width::truncate`] or [`Width::wrap`] and [`Width::increase`] can be used. +//! +//! ## Example +//! +//! ``` +//! use tabled::{Table, settings::Width}; +//! +//! let table = Table::new(&["Hello World!"]) +//! .with(Width::wrap(7)) +//! .with(Width::increase(7)) +//! .to_string(); +//! +//! assert_eq!( +//! table, +//! concat!( +//! "+-----+\n", +//! "| &st |\n", +//! "| r |\n", +//! "+-----+\n", +//! "| Hel |\n", +//! "| lo |\n", +//! "| Wor |\n", +//! "| ld! |\n", +//! "+-----+", +//! ) +//! ); +//! ``` + +mod justify; +mod min_width; +mod truncate; +mod util; +mod width_list; +mod wrap; + +use crate::settings::measurement::Measurement; + +pub use self::{ + justify::Justify, + min_width::MinWidth, + truncate::{SuffixLimit, Truncate}, + width_list::WidthList, + wrap::Wrap, +}; + +/// Width allows you to set a min and max width of an object on a [`Table`] +/// using different strategies. +/// +/// It also allows you to set a min and max width for a whole table. +/// +/// You can apply a min and max strategy at the same time with the same value, +/// the value will be a total table width. +/// +/// It is an abstract factory. +/// +/// Beware that borders are not removed when you set a size value to very small. +/// For example if you set size to 0 the table still be rendered but with all content removed. +/// +/// Also be aware that it doesn't changes [`Padding`] settings nor it considers them. +/// +/// The function is color aware if a `color` feature is on. +/// +/// ## Examples +/// +/// ### Cell change +/// +/// ``` +/// use tabled::{Table, settings::{object::Segment, Width, Style, Modify}}; +/// +/// let data = ["Hello", "World", "!"]; +/// +/// let table = Table::new(&data) +/// .with(Style::markdown()) +/// .with(Modify::new(Segment::all()).with(Width::truncate(3).suffix("..."))); +/// ``` +/// +/// ### Table change +/// +/// ``` +/// use tabled::{Table, settings::Width}; +/// +/// let table = Table::new(&["Hello World!"]).with(Width::wrap(5)); +/// ``` +/// +/// ### Total width +/// +/// ``` +/// use tabled::{Table, settings::Width}; +/// +/// let table = Table::new(&["Hello World!"]) +/// .with(Width::wrap(5)) +/// .with(Width::increase(5)); +/// ``` +/// +/// [`Padding`]: crate::settings::Padding +/// [`Table`]: crate::Table +#[derive(Debug)] +pub struct Width; + +impl Width { + /// Returns a [`Wrap`] structure. + pub fn wrap<W: Measurement<Width>>(width: W) -> Wrap<W> { + Wrap::new(width) + } + + /// Returns a [`Truncate`] structure. + pub fn truncate<W: Measurement<Width>>(width: W) -> Truncate<'static, W> { + Truncate::new(width) + } + + /// Returns a [`MinWidth`] structure. + pub fn increase<W: Measurement<Width>>(width: W) -> MinWidth<W> { + MinWidth::new(width) + } + + /// Returns a [`Justify`] structure. + pub fn justify<W: Measurement<Width>>(width: W) -> Justify<W> { + Justify::new(width) + } + + /// Create [`WidthList`] to set a table width to a constant list of column widths. + /// + /// Notice if you provide a list with `.len()` smaller than `Table::count_columns` then it will have no affect. + /// + /// Also notice that you must provide values bigger than or equal to a real content width, otherwise it may panic. + /// + /// # Example + /// + /// ``` + /// use tabled::{Table, settings::Width}; + /// + /// let data = vec![ + /// ("Some\ndata", "here", "and here"), + /// ("Some\ndata on a next", "line", "right here"), + /// ]; + /// + /// let table = Table::new(data) + /// .with(Width::list([20, 10, 12])) + /// .to_string(); + /// + /// assert_eq!( + /// table, + /// "+--------------------+----------+------------+\n\ + /// | &str | &str | &str |\n\ + /// +--------------------+----------+------------+\n\ + /// | Some | here | and here |\n\ + /// | data | | |\n\ + /// +--------------------+----------+------------+\n\ + /// | Some | line | right here |\n\ + /// | data on a next | | |\n\ + /// +--------------------+----------+------------+" + /// ) + /// ``` + pub fn list<I: IntoIterator<Item = usize>>(rows: I) -> WidthList { + WidthList::new(rows.into_iter().collect()) + } +} diff --git a/vendor/tabled/src/settings/width/truncate.rs b/vendor/tabled/src/settings/width/truncate.rs new file mode 100644 index 000000000..c7336f037 --- /dev/null +++ b/vendor/tabled/src/settings/width/truncate.rs @@ -0,0 +1,505 @@ +//! This module contains [`Truncate`] structure, used to decrease width of a [`Table`]s or a cell on a [`Table`] by truncating the width. +//! +//! [`Table`]: crate::Table + +use std::{borrow::Cow, iter, marker::PhantomData}; + +use crate::{ + grid::{ + config::{ColoredConfig, SpannedConfig}, + dimension::CompleteDimensionVecRecords, + records::{EmptyRecords, ExactRecords, PeekableRecords, Records, RecordsMut}, + util::string::{string_width, string_width_multiline}, + }, + settings::{ + measurement::Measurement, + peaker::{Peaker, PriorityNone}, + CellOption, TableOption, Width, + }, +}; + +use super::util::{cut_str, get_table_widths, get_table_widths_with_total}; + +/// Truncate cut the string to a given width if its length exceeds it. +/// Otherwise keeps the content of a cell untouched. +/// +/// The function is color aware if a `color` feature is on. +/// +/// Be aware that it doesn't consider padding. +/// So if you want to set a exact width you might need to use [`Padding`] to set it to 0. +/// +/// ## Example +/// +/// ``` +/// use tabled::{Table, settings::{object::Segment, Width, Modify}}; +/// +/// let table = Table::new(&["Hello World!"]) +/// .with(Modify::new(Segment::all()).with(Width::truncate(3))); +/// ``` +/// +/// [`Padding`]: crate::settings::Padding +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Truncate<'a, W = usize, P = PriorityNone> { + width: W, + suffix: Option<TruncateSuffix<'a>>, + multiline: bool, + _priority: PhantomData<P>, +} +#[cfg(feature = "color")] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct TruncateSuffix<'a> { + text: Cow<'a, str>, + limit: SuffixLimit, + try_color: bool, +} + +#[cfg(not(feature = "color"))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct TruncateSuffix<'a> { + text: Cow<'a, str>, + limit: SuffixLimit, +} + +impl Default for TruncateSuffix<'_> { + fn default() -> Self { + Self { + text: Cow::default(), + limit: SuffixLimit::Cut, + #[cfg(feature = "color")] + try_color: false, + } + } +} + +/// A suffix limit settings. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum SuffixLimit { + /// Cut the suffix. + Cut, + /// Don't show the suffix. + Ignore, + /// Use a string with n chars instead. + Replace(char), +} + +impl<W> Truncate<'static, W> +where + W: Measurement<Width>, +{ + /// Creates a [`Truncate`] object + pub fn new(width: W) -> Truncate<'static, W> { + Self { + width, + multiline: false, + suffix: None, + _priority: PhantomData, + } + } +} + +impl<'a, W, P> Truncate<'a, W, P> { + /// Sets a suffix which will be appended to a resultant string. + /// + /// The suffix is used in 3 circumstances: + /// 1. If original string is *bigger* than the suffix. + /// We cut more of the original string and append the suffix. + /// 2. If suffix is bigger than the original string. + /// We cut the suffix to fit in the width by default. + /// But you can peak the behaviour by using [`Truncate::suffix_limit`] + pub fn suffix<S: Into<Cow<'a, str>>>(self, suffix: S) -> Truncate<'a, W, P> { + let mut suff = self.suffix.unwrap_or_default(); + suff.text = suffix.into(); + + Truncate { + width: self.width, + multiline: self.multiline, + suffix: Some(suff), + _priority: PhantomData, + } + } + + /// Sets a suffix limit, which is used when the suffix is too big to be used. + pub fn suffix_limit(self, limit: SuffixLimit) -> Truncate<'a, W, P> { + let mut suff = self.suffix.unwrap_or_default(); + suff.limit = limit; + + Truncate { + width: self.width, + multiline: self.multiline, + suffix: Some(suff), + _priority: PhantomData, + } + } + + /// Use trancate logic per line, not as a string as a whole. + pub fn multiline(self) -> Truncate<'a, W, P> { + Truncate { + width: self.width, + multiline: true, + suffix: self.suffix, + _priority: self._priority, + } + } + + #[cfg(feature = "color")] + /// Sets a optional logic to try to colorize a suffix. + pub fn suffix_try_color(self, color: bool) -> Truncate<'a, W, P> { + let mut suff = self.suffix.unwrap_or_default(); + suff.try_color = color; + + Truncate { + width: self.width, + multiline: self.multiline, + suffix: Some(suff), + _priority: PhantomData, + } + } +} + +impl<'a, W, P> Truncate<'a, W, P> { + /// Priority defines the logic by which a truncate will be applied when is done for the whole table. + /// + /// - [`PriorityNone`] which cuts the columns one after another. + /// - [`PriorityMax`] cuts the biggest columns first. + /// - [`PriorityMin`] cuts the lowest columns first. + /// + /// [`PriorityMax`]: crate::settings::peaker::PriorityMax + /// [`PriorityMin`]: crate::settings::peaker::PriorityMin + pub fn priority<PP: Peaker>(self) -> Truncate<'a, W, PP> { + Truncate { + width: self.width, + multiline: self.multiline, + suffix: self.suffix, + _priority: PhantomData, + } + } +} + +impl Truncate<'_, (), ()> { + /// Truncate a given string + pub fn truncate_text(text: &str, width: usize) -> Cow<'_, str> { + truncate_text(text, width, "", false) + } +} + +impl<W, P, R> CellOption<R, ColoredConfig> for Truncate<'_, W, P> +where + W: Measurement<Width>, + R: Records + ExactRecords + PeekableRecords + RecordsMut<String>, + for<'a> &'a R: Records, +{ + fn change(self, records: &mut R, cfg: &mut ColoredConfig, entity: papergrid::config::Entity) { + let available = self.width.measure(&*records, cfg); + + let mut width = available; + let mut suffix = Cow::Borrowed(""); + + if let Some(x) = self.suffix.as_ref() { + let (cutted_suffix, rest_width) = make_suffix(x, width); + suffix = cutted_suffix; + width = rest_width; + }; + + let count_rows = records.count_rows(); + let count_columns = records.count_columns(); + + let colorize = need_suffix_color_preservation(&self.suffix); + + for pos in entity.iter(count_rows, count_columns) { + let is_valid_pos = pos.0 < count_rows && pos.1 < count_columns; + if !is_valid_pos { + continue; + } + + let text = records.get_text(pos); + + let cell_width = string_width_multiline(text); + if available >= cell_width { + continue; + } + + let text = + truncate_multiline(text, &suffix, width, available, colorize, self.multiline); + + records.set(pos, text.into_owned()); + } + } +} + +fn truncate_multiline<'a>( + text: &'a str, + suffix: &'a str, + width: usize, + twidth: usize, + suffix_color: bool, + multiline: bool, +) -> Cow<'a, str> { + if multiline { + let mut buf = String::new(); + for (i, line) in crate::grid::util::string::get_lines(text).enumerate() { + if i != 0 { + buf.push('\n'); + } + + let line = make_text_truncated(&line, suffix, width, twidth, suffix_color); + buf.push_str(&line); + } + + Cow::Owned(buf) + } else { + make_text_truncated(text, suffix, width, twidth, suffix_color) + } +} + +fn make_text_truncated<'a>( + text: &'a str, + suffix: &'a str, + width: usize, + twidth: usize, + suffix_color: bool, +) -> Cow<'a, str> { + if width == 0 { + if twidth == 0 { + Cow::Borrowed("") + } else { + Cow::Borrowed(suffix) + } + } else { + truncate_text(text, width, suffix, suffix_color) + } +} + +fn need_suffix_color_preservation(_suffix: &Option<TruncateSuffix<'_>>) -> bool { + #[cfg(not(feature = "color"))] + { + false + } + #[cfg(feature = "color")] + { + _suffix.as_ref().map_or(false, |s| s.try_color) + } +} + +fn make_suffix<'a>(suffix: &'a TruncateSuffix<'_>, width: usize) -> (Cow<'a, str>, usize) { + let suffix_length = string_width(&suffix.text); + if width > suffix_length { + return (Cow::Borrowed(suffix.text.as_ref()), width - suffix_length); + } + + match suffix.limit { + SuffixLimit::Ignore => (Cow::Borrowed(""), width), + SuffixLimit::Cut => { + let suffix = cut_str(&suffix.text, width); + (suffix, 0) + } + SuffixLimit::Replace(c) => { + let suffix = Cow::Owned(iter::repeat(c).take(width).collect()); + (suffix, 0) + } + } +} + +impl<W, P, R> TableOption<R, CompleteDimensionVecRecords<'static>, ColoredConfig> + for Truncate<'_, W, P> +where + W: Measurement<Width>, + P: Peaker, + R: Records + ExactRecords + PeekableRecords + RecordsMut<String>, + for<'a> &'a R: Records, +{ + fn change( + self, + records: &mut R, + cfg: &mut ColoredConfig, + dims: &mut CompleteDimensionVecRecords<'static>, + ) { + if records.count_rows() == 0 || records.count_columns() == 0 { + return; + } + + let width = self.width.measure(&*records, cfg); + let (widths, total) = get_table_widths_with_total(&*records, cfg); + if total <= width { + return; + } + + let suffix = self.suffix.as_ref().map(|s| TruncateSuffix { + text: Cow::Borrowed(&s.text), + limit: s.limit, + #[cfg(feature = "color")] + try_color: s.try_color, + }); + + let priority = P::create(); + let multiline = self.multiline; + let widths = truncate_total_width( + records, cfg, widths, total, width, priority, suffix, multiline, + ); + + let _ = dims.set_widths(widths); + } +} + +#[allow(clippy::too_many_arguments)] +fn truncate_total_width<P, R>( + records: &mut R, + cfg: &mut ColoredConfig, + mut widths: Vec<usize>, + total: usize, + width: usize, + priority: P, + suffix: Option<TruncateSuffix<'_>>, + multiline: bool, +) -> Vec<usize> +where + for<'a> &'a R: Records, + P: Peaker, + R: Records + PeekableRecords + ExactRecords + RecordsMut<String>, +{ + let count_rows = records.count_rows(); + let count_columns = records.count_columns(); + + let min_widths = get_table_widths(EmptyRecords::new(count_rows, count_columns), cfg); + + decrease_widths(&mut widths, &min_widths, total, width, priority); + + let points = get_decrease_cell_list(cfg, &widths, &min_widths, (count_rows, count_columns)); + + for ((row, col), width) in points { + let mut truncate = Truncate::new(width); + truncate.suffix = suffix.clone(); + truncate.multiline = multiline; + CellOption::change(truncate, records, cfg, (row, col).into()); + } + + widths +} + +fn truncate_text<'a>( + text: &'a str, + width: usize, + suffix: &str, + _suffix_color: bool, +) -> Cow<'a, str> { + let content = cut_str(text, width); + if suffix.is_empty() { + return content; + } + + #[cfg(feature = "color")] + { + if _suffix_color { + if let Some(block) = ansi_str::get_blocks(text).last() { + if block.has_ansi() { + let style = block.style(); + Cow::Owned(format!( + "{}{}{}{}", + content, + style.start(), + suffix, + style.end() + )) + } else { + let mut content = content.into_owned(); + content.push_str(suffix); + Cow::Owned(content) + } + } else { + let mut content = content.into_owned(); + content.push_str(suffix); + Cow::Owned(content) + } + } else { + let mut content = content.into_owned(); + content.push_str(suffix); + Cow::Owned(content) + } + } + + #[cfg(not(feature = "color"))] + { + let mut content = content.into_owned(); + content.push_str(suffix); + Cow::Owned(content) + } +} + +fn get_decrease_cell_list( + cfg: &SpannedConfig, + widths: &[usize], + min_widths: &[usize], + shape: (usize, usize), +) -> Vec<((usize, usize), usize)> { + let mut points = Vec::new(); + (0..shape.1).for_each(|col| { + (0..shape.0) + .filter(|&row| cfg.is_cell_visible((row, col))) + .for_each(|row| { + let (width, width_min) = match cfg.get_column_span((row, col)) { + Some(span) => { + let width = (col..col + span).map(|i| widths[i]).sum::<usize>(); + let min_width = (col..col + span).map(|i| min_widths[i]).sum::<usize>(); + let count_borders = count_borders(cfg, col, col + span, shape.1); + (width + count_borders, min_width + count_borders) + } + None => (widths[col], min_widths[col]), + }; + + if width >= width_min { + let padding = cfg.get_padding((row, col).into()); + let width = width.saturating_sub(padding.left.size + padding.right.size); + + points.push(((row, col), width)); + } + }); + }); + + points +} + +fn decrease_widths<F>( + widths: &mut [usize], + min_widths: &[usize], + total_width: usize, + mut width: usize, + mut peeaker: F, +) where + F: Peaker, +{ + let mut empty_list = 0; + for col in 0..widths.len() { + if widths[col] == 0 || widths[col] <= min_widths[col] { + empty_list += 1; + } + } + + while width != total_width { + if empty_list == widths.len() { + break; + } + + let col = match peeaker.peak(min_widths, widths) { + Some(col) => col, + None => break, + }; + + if widths[col] == 0 || widths[col] <= min_widths[col] { + continue; + } + + widths[col] -= 1; + + if widths[col] == 0 || widths[col] <= min_widths[col] { + empty_list += 1; + } + + width += 1; + } +} + +fn count_borders(cfg: &SpannedConfig, start: usize, end: usize, count_columns: usize) -> usize { + (start..end) + .skip(1) + .filter(|&i| cfg.has_vertical(i, count_columns)) + .count() +} diff --git a/vendor/tabled/src/settings/width/util.rs b/vendor/tabled/src/settings/width/util.rs new file mode 100644 index 000000000..92cc48c41 --- /dev/null +++ b/vendor/tabled/src/settings/width/util.rs @@ -0,0 +1,265 @@ +use std::borrow::Cow; + +use crate::{ + grid::config::SpannedConfig, grid::dimension::SpannedGridDimension, grid::records::Records, +}; + +pub(crate) fn get_table_widths<R: Records>(records: R, cfg: &SpannedConfig) -> Vec<usize> { + SpannedGridDimension::width(records, cfg) +} + +pub(crate) fn get_table_widths_with_total<R: Records>( + records: R, + cfg: &SpannedConfig, +) -> (Vec<usize>, usize) { + let widths = SpannedGridDimension::width(records, cfg); + let total_width = get_table_total_width(&widths, cfg); + (widths, total_width) +} + +fn get_table_total_width(list: &[usize], cfg: &SpannedConfig) -> usize { + let margin = cfg.get_margin(); + list.iter().sum::<usize>() + + cfg.count_vertical(list.len()) + + margin.left.size + + margin.right.size +} + +/// The function cuts the string to a specific width. +/// +/// BE AWARE: width is expected to be in bytes. +pub(crate) fn cut_str(s: &str, width: usize) -> Cow<'_, str> { + #[cfg(feature = "color")] + { + const REPLACEMENT: char = '\u{FFFD}'; + + let stripped = ansi_str::AnsiStr::ansi_strip(s); + let (length, count_unknowns, _) = split_at_pos(&stripped, width); + + let mut buf = ansi_str::AnsiStr::ansi_cut(s, ..length); + if count_unknowns > 0 { + let mut b = buf.into_owned(); + b.extend(std::iter::repeat(REPLACEMENT).take(count_unknowns)); + buf = Cow::Owned(b); + } + + buf + } + + #[cfg(not(feature = "color"))] + { + cut_str_basic(s, width) + } +} + +/// The function cuts the string to a specific width. +/// +/// BE AWARE: width is expected to be in bytes. +#[cfg(not(feature = "color"))] +pub(crate) fn cut_str_basic(s: &str, width: usize) -> Cow<'_, str> { + const REPLACEMENT: char = '\u{FFFD}'; + + let (length, count_unknowns, _) = split_at_pos(s, width); + let buf = &s[..length]; + if count_unknowns == 0 { + return Cow::Borrowed(buf); + } + + let mut buf = buf.to_owned(); + buf.extend(std::iter::repeat(REPLACEMENT).take(count_unknowns)); + + Cow::Owned(buf) +} + +/// The function splits a string in the position and +/// returns a exact number of bytes before the position and in case of a split in an unicode grapheme +/// a width of a character which was tried to be splited in. +/// +/// BE AWARE: pos is expected to be in bytes. +pub(crate) fn split_at_pos(s: &str, pos: usize) -> (usize, usize, usize) { + let mut length = 0; + let mut i = 0; + for c in s.chars() { + if i == pos { + break; + }; + + let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or_default(); + + // We cut the chars which takes more then 1 symbol to display, + // in order to archive the necessary width. + if i + c_width > pos { + let count = pos - i; + return (length, count, c.len_utf8()); + } + + i += c_width; + length += c.len_utf8(); + } + + (length, 0, 0) +} + +/// Strip OSC codes from `s`. If `s` is a single OSC8 hyperlink, with no other text, then return +/// (s_with_all_hyperlinks_removed, Some(url)). If `s` does not meet this description, then return +/// (s_with_all_hyperlinks_removed, None). Any ANSI color sequences in `s` will be retained. See +/// <https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda> +/// +/// The function is based on Dan Davison <https://github.com/dandavison> delta <https://github.com/dandavison/delta> ansi library. +#[cfg(feature = "color")] +pub(crate) fn strip_osc(text: &str) -> (String, Option<String>) { + #[derive(Debug)] + enum ExtractOsc8HyperlinkState { + ExpectOsc8Url, + ExpectFirstText, + ExpectMoreTextOrTerminator, + SeenOneHyperlink, + WillNotReturnUrl, + } + + use ExtractOsc8HyperlinkState::*; + + let mut url = None; + let mut state = ExpectOsc8Url; + let mut buf = String::with_capacity(text.len()); + + for el in ansitok::parse_ansi(text) { + match el.kind() { + ansitok::ElementKind::Osc => match state { + ExpectOsc8Url => { + url = Some(&text[el.start()..el.end()]); + state = ExpectFirstText; + } + ExpectMoreTextOrTerminator => state = SeenOneHyperlink, + _ => state = WillNotReturnUrl, + }, + ansitok::ElementKind::Sgr => buf.push_str(&text[el.start()..el.end()]), + ansitok::ElementKind::Csi => buf.push_str(&text[el.start()..el.end()]), + ansitok::ElementKind::Esc => {} + ansitok::ElementKind::Text => { + buf.push_str(&text[el.start()..el.end()]); + match state { + ExpectFirstText => state = ExpectMoreTextOrTerminator, + ExpectMoreTextOrTerminator => {} + _ => state = WillNotReturnUrl, + } + } + } + } + + match state { + WillNotReturnUrl => (buf, None), + _ => { + let url = url.and_then(|s| { + s.strip_prefix("\x1b]8;;") + .and_then(|s| s.strip_suffix('\x1b')) + }); + if let Some(url) = url { + (buf, Some(url.to_string())) + } else { + (buf, None) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::grid::util::string::string_width; + + #[cfg(feature = "color")] + use owo_colors::{colors::Yellow, OwoColorize}; + + #[test] + fn strip_test() { + assert_eq!(cut_str("123456", 0), ""); + assert_eq!(cut_str("123456", 3), "123"); + assert_eq!(cut_str("123456", 10), "123456"); + + assert_eq!(cut_str("a week ago", 4), "a we"); + + assert_eq!(cut_str("π³π³π³π³π³", 0), ""); + assert_eq!(cut_str("π³π³π³π³π³", 3), "π³οΏ½"); + assert_eq!(cut_str("π³π³π³π³π³", 4), "π³π³"); + assert_eq!(cut_str("π³π³π³π³π³", 20), "π³π³π³π³π³"); + + assert_eq!(cut_str("π³οΈπ³οΈ", 0), ""); + assert_eq!(cut_str("π³οΈπ³οΈ", 1), "π³"); + assert_eq!(cut_str("π³οΈπ³οΈ", 2), "π³\u{fe0f}π³"); + assert_eq!(string_width("π³οΈπ³οΈ"), string_width("π³\u{fe0f}π³")); + + assert_eq!(cut_str("π", 1), "οΏ½"); + assert_eq!(cut_str("π", 2), "π"); + + assert_eq!(cut_str("π₯Ώ", 1), "οΏ½"); + assert_eq!(cut_str("π₯Ώ", 2), "π₯Ώ"); + + assert_eq!(cut_str("π©°", 1), "οΏ½"); + assert_eq!(cut_str("π©°", 2), "π©°"); + + assert_eq!(cut_str("ππΏ", 1), "οΏ½"); + assert_eq!(cut_str("ππΏ", 2), "π"); + assert_eq!(cut_str("ππΏ", 3), "ποΏ½"); + assert_eq!(cut_str("ππΏ", 4), "ππΏ"); + + assert_eq!(cut_str("π»π¬", 1), "π»"); + assert_eq!(cut_str("π»π¬", 2), "π»π¬"); + assert_eq!(cut_str("π»π¬", 3), "π»π¬"); + assert_eq!(cut_str("π»π¬", 4), "π»π¬"); + } + + #[cfg(feature = "color")] + #[test] + fn strip_color_test() { + let numbers = "123456".red().on_bright_black().to_string(); + + assert_eq!(cut_str(&numbers, 0), "\u{1b}[31;100m\u{1b}[39m\u{1b}[49m"); + assert_eq!( + cut_str(&numbers, 3), + "\u{1b}[31;100m123\u{1b}[39m\u{1b}[49m" + ); + assert_eq!(cut_str(&numbers, 10), "\u{1b}[31;100m123456\u{1b}[0m"); + + let emojies = "π³π³π³π³π³".red().on_bright_black().to_string(); + + assert_eq!(cut_str(&emojies, 0), "\u{1b}[31;100m\u{1b}[39m\u{1b}[49m"); + assert_eq!( + cut_str(&emojies, 3), + "\u{1b}[31;100mπ³\u{1b}[39m\u{1b}[49mοΏ½" + ); + assert_eq!( + cut_str(&emojies, 4), + "\u{1b}[31;100mπ³π³\u{1b}[39m\u{1b}[49m" + ); + assert_eq!(cut_str(&emojies, 20), "\u{1b}[31;100mπ³π³π³π³π³\u{1b}[0m"); + + let emojies = "π³οΈπ³οΈ".red().on_bright_black().to_string(); + + assert_eq!(cut_str(&emojies, 0), "\u{1b}[31;100m\u{1b}[39m\u{1b}[49m"); + assert_eq!(cut_str(&emojies, 1), "\u{1b}[31;100mπ³\u{1b}[39m\u{1b}[49m"); + assert_eq!( + cut_str(&emojies, 2), + "\u{1b}[31;100mπ³\u{fe0f}π³\u{1b}[39m\u{1b}[49m" + ); + assert_eq!( + string_width(&emojies), + string_width("\u{1b}[31;100mπ³\u{fe0f}π³\u{1b}[39m\u{1b}[49m") + ); + } + + #[test] + #[cfg(feature = "color")] + fn test_color_strip() { + let s = "Collored string" + .fg::<Yellow>() + .on_truecolor(12, 200, 100) + .blink() + .to_string(); + assert_eq!( + cut_str(&s, 1), + "\u{1b}[5m\u{1b}[48;2;12;200;100m\u{1b}[33mC\u{1b}[25m\u{1b}[39m\u{1b}[49m" + ) + } +} diff --git a/vendor/tabled/src/settings/width/width_list.rs b/vendor/tabled/src/settings/width/width_list.rs new file mode 100644 index 000000000..7547b97f3 --- /dev/null +++ b/vendor/tabled/src/settings/width/width_list.rs @@ -0,0 +1,50 @@ +use std::iter::FromIterator; + +use crate::{ + grid::dimension::CompleteDimensionVecRecords, grid::records::Records, settings::TableOption, +}; + +/// A structure used to set [`Table`] width via a list of columns widths. +/// +/// [`Table`]: crate::Table +#[derive(Debug)] +pub struct WidthList { + list: Vec<usize>, +} + +impl WidthList { + /// Creates a new object. + pub fn new(list: Vec<usize>) -> Self { + Self { list } + } +} + +impl From<Vec<usize>> for WidthList { + fn from(list: Vec<usize>) -> Self { + Self::new(list) + } +} + +impl FromIterator<usize> for WidthList { + fn from_iter<T: IntoIterator<Item = usize>>(iter: T) -> Self { + Self::new(iter.into_iter().collect()) + } +} + +impl<R, C> TableOption<R, CompleteDimensionVecRecords<'static>, C> for WidthList +where + R: Records, +{ + fn change( + self, + records: &mut R, + _: &mut C, + dimension: &mut CompleteDimensionVecRecords<'static>, + ) { + if self.list.len() < records.count_columns() { + return; + } + + let _ = dimension.set_widths(self.list); + } +} diff --git a/vendor/tabled/src/settings/width/wrap.rs b/vendor/tabled/src/settings/width/wrap.rs new file mode 100644 index 000000000..96a370408 --- /dev/null +++ b/vendor/tabled/src/settings/width/wrap.rs @@ -0,0 +1,1468 @@ +//! This module contains [`Wrap`] structure, used to decrease width of a [`Table`]s or a cell on a [`Table`] by wrapping it's content +//! to a new line. +//! +//! [`Table`]: crate::Table + +use std::marker::PhantomData; + +use crate::{ + grid::config::ColoredConfig, + grid::dimension::CompleteDimensionVecRecords, + grid::records::{EmptyRecords, ExactRecords, PeekableRecords, Records, RecordsMut}, + grid::{config::Entity, config::SpannedConfig, util::string::string_width_multiline}, + settings::{ + measurement::Measurement, + peaker::{Peaker, PriorityNone}, + width::Width, + CellOption, TableOption, + }, +}; + +use super::util::{get_table_widths, get_table_widths_with_total, split_at_pos}; + +/// Wrap wraps a string to a new line in case it exceeds the provided max boundary. +/// Otherwise keeps the content of a cell untouched. +/// +/// The function is color aware if a `color` feature is on. +/// +/// Be aware that it doesn't consider padding. +/// So if you want to set a exact width you might need to use [`Padding`] to set it to 0. +/// +/// ## Example +/// +/// ``` +/// use tabled::{Table, settings::{object::Segment, width::Width, Modify}}; +/// +/// let table = Table::new(&["Hello World!"]) +/// .with(Modify::new(Segment::all()).with(Width::wrap(3))); +/// ``` +/// +/// [`Padding`]: crate::settings::Padding +#[derive(Debug, Clone)] +pub struct Wrap<W = usize, P = PriorityNone> { + width: W, + keep_words: bool, + _priority: PhantomData<P>, +} + +impl<W> Wrap<W> { + /// Creates a [`Wrap`] object + pub fn new(width: W) -> Self + where + W: Measurement<Width>, + { + Wrap { + width, + keep_words: false, + _priority: PhantomData, + } + } +} + +impl<W, P> Wrap<W, P> { + /// Priority defines the logic by which a truncate will be applied when is done for the whole table. + /// + /// - [`PriorityNone`] which cuts the columns one after another. + /// - [`PriorityMax`] cuts the biggest columns first. + /// - [`PriorityMin`] cuts the lowest columns first. + /// + /// Be aware that it doesn't consider padding. + /// So if you want to set a exact width you might need to use [`Padding`] to set it to 0. + /// + /// [`Padding`]: crate::settings::Padding + /// [`PriorityMax`]: crate::settings::peaker::PriorityMax + /// [`PriorityMin`]: crate::settings::peaker::PriorityMin + pub fn priority<PP>(self) -> Wrap<W, PP> { + Wrap { + width: self.width, + keep_words: self.keep_words, + _priority: PhantomData, + } + } + + /// Set the keep words option. + /// + /// If a wrapping point will be in a word, [`Wrap`] will + /// preserve a word (if possible) and wrap the string before it. + pub fn keep_words(mut self) -> Self { + self.keep_words = true; + self + } +} + +impl Wrap<(), ()> { + /// Wrap a given string + pub fn wrap_text(text: &str, width: usize, keeping_words: bool) -> String { + wrap_text(text, width, keeping_words) + } +} + +impl<W, P, R> TableOption<R, CompleteDimensionVecRecords<'static>, ColoredConfig> for Wrap<W, P> +where + W: Measurement<Width>, + P: Peaker, + R: Records + ExactRecords + PeekableRecords + RecordsMut<String>, + for<'a> &'a R: Records, +{ + fn change( + self, + records: &mut R, + cfg: &mut ColoredConfig, + dims: &mut CompleteDimensionVecRecords<'static>, + ) { + if records.count_rows() == 0 || records.count_columns() == 0 { + return; + } + + let width = self.width.measure(&*records, cfg); + let (widths, total) = get_table_widths_with_total(&*records, cfg); + if width >= total { + return; + } + + let priority = P::create(); + let keep_words = self.keep_words; + let widths = wrap_total_width(records, cfg, widths, total, width, keep_words, priority); + + let _ = dims.set_widths(widths); + } +} + +impl<W, R> CellOption<R, ColoredConfig> for Wrap<W> +where + W: Measurement<Width>, + R: Records + ExactRecords + PeekableRecords + RecordsMut<String>, + for<'a> &'a R: Records, +{ + fn change(self, records: &mut R, cfg: &mut ColoredConfig, entity: Entity) { + let width = self.width.measure(&*records, cfg); + + let count_rows = records.count_rows(); + let count_columns = records.count_columns(); + + for pos in entity.iter(count_rows, count_columns) { + let is_valid_pos = pos.0 < records.count_rows() && pos.1 < records.count_columns(); + if !is_valid_pos { + continue; + } + + let text = records.get_text(pos); + let cell_width = string_width_multiline(text); + if cell_width <= width { + continue; + } + + let wrapped = wrap_text(text, width, self.keep_words); + records.set(pos, wrapped); + } + } +} + +fn wrap_total_width<R, P>( + records: &mut R, + cfg: &mut ColoredConfig, + mut widths: Vec<usize>, + total_width: usize, + width: usize, + keep_words: bool, + priority: P, +) -> Vec<usize> +where + R: Records + ExactRecords + PeekableRecords + RecordsMut<String>, + P: Peaker, + for<'a> &'a R: Records, +{ + let shape = (records.count_rows(), records.count_columns()); + let min_widths = get_table_widths(EmptyRecords::from(shape), cfg); + + decrease_widths(&mut widths, &min_widths, total_width, width, priority); + + let points = get_decrease_cell_list(cfg, &widths, &min_widths, shape); + + for ((row, col), width) in points { + let mut wrap = Wrap::new(width); + wrap.keep_words = keep_words; + <Wrap as CellOption<_, _>>::change(wrap, records, cfg, (row, col).into()); + } + + widths +} + +#[cfg(not(feature = "color"))] +pub(crate) fn wrap_text(text: &str, width: usize, keep_words: bool) -> String { + if width == 0 { + return String::new(); + } + + if keep_words { + split_keeping_words(text, width, "\n") + } else { + chunks(text, width).join("\n") + } +} + +#[cfg(feature = "color")] +pub(crate) fn wrap_text(text: &str, width: usize, keep_words: bool) -> String { + use super::util::strip_osc; + + if width == 0 { + return String::new(); + } + + let (text, url): (String, Option<String>) = strip_osc(text); + let (prefix, suffix) = build_link_prefix_suffix(url); + + if keep_words { + split_keeping_words(&text, width, &prefix, &suffix) + } else { + chunks(&text, width, &prefix, &suffix).join("\n") + } +} + +#[cfg(feature = "color")] +fn build_link_prefix_suffix(url: Option<String>) -> (String, String) { + match url { + Some(url) => { + // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda + let osc8 = "\x1b]8;;"; + let st = "\x1b\\"; + + (format!("{osc8}{url}{st}"), format!("{osc8}{st}")) + } + None => ("".to_string(), "".to_string()), + } +} + +#[cfg(not(feature = "color"))] +fn chunks(s: &str, width: usize) -> Vec<String> { + if width == 0 { + return Vec::new(); + } + + const REPLACEMENT: char = '\u{FFFD}'; + + let mut buf = String::with_capacity(width); + let mut list = Vec::new(); + let mut i = 0; + for c in s.chars() { + let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or_default(); + if i + c_width > width { + let count_unknowns = width - i; + buf.extend(std::iter::repeat(REPLACEMENT).take(count_unknowns)); + i += count_unknowns; + } else { + buf.push(c); + i += c_width; + } + + if i == width { + list.push(buf); + buf = String::with_capacity(width); + i = 0; + } + } + + if !buf.is_empty() { + list.push(buf); + } + + list +} + +#[cfg(feature = "color")] +fn chunks(s: &str, width: usize, prefix: &str, suffix: &str) -> Vec<String> { + use std::fmt::Write; + + if width == 0 { + return Vec::new(); + } + + let mut list = Vec::new(); + let mut line = String::with_capacity(width); + let mut line_width = 0; + + for b in ansi_str::get_blocks(s) { + let text_style = b.style(); + let mut text_slice = b.text(); + if text_slice.is_empty() { + continue; + } + + let available_space = width - line_width; + if available_space == 0 { + list.push(line); + line = String::with_capacity(width); + line_width = 0; + } + + line.push_str(prefix); + let _ = write!(&mut line, "{}", text_style.start()); + + while !text_slice.is_empty() { + let available_space = width - line_width; + + let part_width = unicode_width::UnicodeWidthStr::width(text_slice); + if part_width <= available_space { + line.push_str(text_slice); + line_width += part_width; + + if available_space == 0 { + let _ = write!(&mut line, "{}", text_style.end()); + line.push_str(suffix); + list.push(line); + line = String::with_capacity(width); + line.push_str(prefix); + line_width = 0; + let _ = write!(&mut line, "{}", text_style.start()); + } + + break; + } + + let (lhs, rhs, (unknowns, split_char)) = split_string_at(text_slice, available_space); + + text_slice = &rhs[split_char..]; + + line.push_str(lhs); + line_width += unicode_width::UnicodeWidthStr::width(lhs); + + const REPLACEMENT: char = '\u{FFFD}'; + line.extend(std::iter::repeat(REPLACEMENT).take(unknowns)); + line_width += unknowns; + + if line_width == width { + let _ = write!(&mut line, "{}", text_style.end()); + line.push_str(suffix); + list.push(line); + line = String::with_capacity(width); + line.push_str(prefix); + line_width = 0; + let _ = write!(&mut line, "{}", text_style.start()); + } + } + + if line_width > 0 { + let _ = write!(&mut line, "{}", text_style.end()); + } + } + + if line_width > 0 { + line.push_str(suffix); + list.push(line); + } + + list +} + +#[cfg(not(feature = "color"))] +fn split_keeping_words(s: &str, width: usize, sep: &str) -> String { + const REPLACEMENT: char = '\u{FFFD}'; + + let mut lines = Vec::new(); + let mut line = String::with_capacity(width); + let mut line_width = 0; + + let mut is_first_word = true; + + for word in s.split(' ') { + if !is_first_word { + let line_has_space = line_width < width; + if line_has_space { + line.push(' '); + line_width += 1; + is_first_word = false; + } + } + + if is_first_word { + is_first_word = false; + } + + let word_width = unicode_width::UnicodeWidthStr::width(word); + + let line_has_space = line_width + word_width <= width; + if line_has_space { + line.push_str(word); + line_width += word_width; + continue; + } + + if word_width <= width { + // the word can be fit to 'width' so we put it on new line + + line.extend(std::iter::repeat(' ').take(width - line_width)); + lines.push(line); + + line = String::with_capacity(width); + line_width = 0; + + line.push_str(word); + line_width += word_width; + is_first_word = false; + } else { + // the word is too long any way so we split it + + let mut word_part = word; + while !word_part.is_empty() { + let available_space = width - line_width; + let (lhs, rhs, (unknowns, split_char)) = + split_string_at(word_part, available_space); + + word_part = &rhs[split_char..]; + line_width += unicode_width::UnicodeWidthStr::width(lhs) + unknowns; + is_first_word = false; + + line.push_str(lhs); + line.extend(std::iter::repeat(REPLACEMENT).take(unknowns)); + + if line_width == width { + lines.push(line); + line = String::with_capacity(width); + line_width = 0; + is_first_word = true; + } + } + } + } + + if line_width > 0 { + line.extend(std::iter::repeat(' ').take(width - line_width)); + lines.push(line); + } + + lines.join(sep) +} + +#[cfg(feature = "color")] +fn split_keeping_words(text: &str, width: usize, prefix: &str, suffix: &str) -> String { + if text.is_empty() || width == 0 { + return String::new(); + } + + let stripped_text = ansi_str::AnsiStr::ansi_strip(text); + let mut word_width = 0; + let mut word_chars = 0; + let mut blocks = parsing::Blocks::new(ansi_str::get_blocks(text)); + let mut buf = parsing::MultilineBuffer::new(width); + buf.set_prefix(prefix); + buf.set_suffix(suffix); + + for c in stripped_text.chars() { + match c { + ' ' => { + parsing::handle_word(&mut buf, &mut blocks, word_chars, word_width, 1); + word_chars = 0; + word_width = 0; + } + '\n' => { + parsing::handle_word(&mut buf, &mut blocks, word_chars, word_width, 1); + word_chars = 0; + word_width = 0; + } + _ => { + word_width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0); + word_chars += 1; + } + } + } + + if word_chars > 0 { + parsing::handle_word(&mut buf, &mut blocks, word_chars, word_width, 0); + buf.finish_line(&blocks); + } + + buf.into_string() +} + +#[cfg(feature = "color")] +mod parsing { + use ansi_str::{AnsiBlock, AnsiBlockIter, Style}; + use std::fmt::Write; + + pub(super) struct Blocks<'a> { + iter: AnsiBlockIter<'a>, + current: Option<RelativeBlock<'a>>, + } + + impl<'a> Blocks<'a> { + pub(super) fn new(iter: AnsiBlockIter<'a>) -> Self { + Self { + iter, + current: None, + } + } + + pub(super) fn next_block(&mut self) -> Option<RelativeBlock<'a>> { + self.current + .take() + .or_else(|| self.iter.next().map(RelativeBlock::new)) + } + } + + pub(super) struct RelativeBlock<'a> { + block: AnsiBlock<'a>, + pos: usize, + } + + impl<'a> RelativeBlock<'a> { + pub(super) fn new(block: AnsiBlock<'a>) -> Self { + Self { block, pos: 0 } + } + + pub(super) fn get_text(&self) -> &str { + &self.block.text()[self.pos..] + } + + pub(super) fn get_origin(&self) -> &str { + self.block.text() + } + + pub(super) fn get_style(&self) -> &Style { + self.block.style() + } + } + + pub(super) struct MultilineBuffer<'a> { + buf: String, + width_last: usize, + width: usize, + prefix: &'a str, + suffix: &'a str, + } + + impl<'a> MultilineBuffer<'a> { + pub(super) fn new(width: usize) -> Self { + Self { + buf: String::new(), + width_last: 0, + prefix: "", + suffix: "", + width, + } + } + + pub(super) fn into_string(self) -> String { + self.buf + } + + pub(super) fn set_suffix(&mut self, suffix: &'a str) { + self.suffix = suffix; + } + + pub(super) fn set_prefix(&mut self, prefix: &'a str) { + self.prefix = prefix; + } + + pub(super) fn max_width(&self) -> usize { + self.width + } + + pub(super) fn available_width(&self) -> usize { + self.width - self.width_last + } + + pub(super) fn fill(&mut self, c: char) -> usize { + debug_assert_eq!(unicode_width::UnicodeWidthChar::width(c), Some(1)); + + let rest_width = self.available_width(); + for _ in 0..rest_width { + self.buf.push(c); + } + + rest_width + } + + pub(super) fn set_next_line(&mut self, blocks: &Blocks<'_>) { + if let Some(block) = &blocks.current { + let _ = self + .buf + .write_fmt(format_args!("{}", block.get_style().end())); + } + + self.buf.push_str(self.suffix); + + let _ = self.fill(' '); + self.buf.push('\n'); + self.width_last = 0; + + self.buf.push_str(self.prefix); + + if let Some(block) = &blocks.current { + let _ = self + .buf + .write_fmt(format_args!("{}", block.get_style().start())); + } + } + + pub(super) fn finish_line(&mut self, blocks: &Blocks<'_>) { + if let Some(block) = &blocks.current { + let _ = self + .buf + .write_fmt(format_args!("{}", block.get_style().end())); + } + + self.buf.push_str(self.suffix); + + let _ = self.fill(' '); + self.width_last = 0; + } + + pub(super) fn read_chars(&mut self, block: &RelativeBlock<'_>, n: usize) -> (usize, usize) { + let mut count_chars = 0; + let mut count_bytes = 0; + for c in block.get_text().chars() { + if count_chars == n { + break; + } + + count_chars += 1; + count_bytes += c.len_utf8(); + + let cwidth = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0); + + let available_space = self.width - self.width_last; + if available_space == 0 { + let _ = self + .buf + .write_fmt(format_args!("{}", block.get_style().end())); + self.buf.push_str(self.suffix); + self.buf.push('\n'); + self.buf.push_str(self.prefix); + let _ = self + .buf + .write_fmt(format_args!("{}", block.get_style().start())); + self.width_last = 0; + } + + let is_enough_space = self.width_last + cwidth <= self.width; + if !is_enough_space { + // thereatically a cwidth can be 2 but buf_width is 1 + // but it handled here too; + + const REPLACEMENT: char = '\u{FFFD}'; + let _ = self.fill(REPLACEMENT); + self.width_last = self.width; + } else { + self.buf.push(c); + self.width_last += cwidth; + } + } + + (count_chars, count_bytes) + } + + pub(super) fn read_chars_unchecked( + &mut self, + block: &RelativeBlock<'_>, + n: usize, + ) -> (usize, usize) { + let mut count_chars = 0; + let mut count_bytes = 0; + for c in block.get_text().chars() { + if count_chars == n { + break; + } + + count_chars += 1; + count_bytes += c.len_utf8(); + + let cwidth = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0); + self.width_last += cwidth; + + self.buf.push(c); + } + + debug_assert!(self.width_last <= self.width); + + (count_chars, count_bytes) + } + } + + pub(super) fn read_chars(buf: &mut MultilineBuffer<'_>, blocks: &mut Blocks<'_>, n: usize) { + let mut n = n; + while n > 0 { + let is_new_block = blocks.current.is_none(); + let mut block = blocks.next_block().expect("Must never happen"); + if is_new_block { + buf.buf.push_str(buf.prefix); + let _ = buf + .buf + .write_fmt(format_args!("{}", block.get_style().start())); + } + + let (read_count, read_bytes) = buf.read_chars(&block, n); + + if block.pos + read_bytes == block.get_origin().len() { + let _ = buf + .buf + .write_fmt(format_args!("{}", block.get_style().end())); + } else { + block.pos += read_bytes; + blocks.current = Some(block); + } + + n -= read_count; + } + } + + pub(super) fn read_chars_unchecked( + buf: &mut MultilineBuffer<'_>, + blocks: &mut Blocks<'_>, + n: usize, + ) { + let mut n = n; + while n > 0 { + let is_new_block = blocks.current.is_none(); + let mut block = blocks.next_block().expect("Must never happen"); + + if is_new_block { + buf.buf.push_str(buf.prefix); + let _ = buf + .buf + .write_fmt(format_args!("{}", block.get_style().start())); + } + + let (read_count, read_bytes) = buf.read_chars_unchecked(&block, n); + + if block.pos + read_bytes == block.get_origin().len() { + let _ = buf + .buf + .write_fmt(format_args!("{}", block.get_style().end())); + } else { + block.pos += read_bytes; + blocks.current = Some(block); + } + + n -= read_count; + } + } + + pub(super) fn handle_word( + buf: &mut MultilineBuffer<'_>, + blocks: &mut Blocks<'_>, + word_chars: usize, + word_width: usize, + additional_read: usize, + ) { + if word_chars > 0 { + let has_line_space = word_width <= buf.available_width(); + let is_word_too_big = word_width > buf.max_width(); + + if is_word_too_big { + read_chars(buf, blocks, word_chars + additional_read); + } else if has_line_space { + read_chars_unchecked(buf, blocks, word_chars); + if additional_read > 0 { + read_chars(buf, blocks, additional_read); + } + } else { + buf.set_next_line(&*blocks); + read_chars_unchecked(buf, blocks, word_chars); + if additional_read > 0 { + read_chars(buf, blocks, additional_read); + } + } + + return; + } + + let has_current_line_space = additional_read <= buf.available_width(); + if has_current_line_space { + read_chars_unchecked(buf, blocks, additional_read); + } else { + buf.set_next_line(&*blocks); + read_chars_unchecked(buf, blocks, additional_read); + } + } +} + +fn split_string_at(text: &str, at: usize) -> (&str, &str, (usize, usize)) { + let (length, count_unknowns, split_char_size) = split_at_pos(text, at); + let (lhs, rhs) = text.split_at(length); + + (lhs, rhs, (count_unknowns, split_char_size)) +} + +fn decrease_widths<F>( + widths: &mut [usize], + min_widths: &[usize], + total_width: usize, + mut width: usize, + mut peeaker: F, +) where + F: Peaker, +{ + let mut empty_list = 0; + for col in 0..widths.len() { + if widths[col] == 0 || widths[col] <= min_widths[col] { + empty_list += 1; + } + } + + while width != total_width { + if empty_list == widths.len() { + break; + } + + let col = match peeaker.peak(min_widths, widths) { + Some(col) => col, + None => break, + }; + + if widths[col] == 0 || widths[col] <= min_widths[col] { + continue; + } + + widths[col] -= 1; + + if widths[col] == 0 || widths[col] <= min_widths[col] { + empty_list += 1; + } + + width += 1; + } +} + +fn get_decrease_cell_list( + cfg: &SpannedConfig, + widths: &[usize], + min_widths: &[usize], + shape: (usize, usize), +) -> Vec<((usize, usize), usize)> { + let mut points = Vec::new(); + (0..shape.1).for_each(|col| { + (0..shape.0) + .filter(|&row| cfg.is_cell_visible((row, col))) + .for_each(|row| { + let (width, width_min) = match cfg.get_column_span((row, col)) { + Some(span) => { + let width = (col..col + span).map(|i| widths[i]).sum::<usize>(); + let min_width = (col..col + span).map(|i| min_widths[i]).sum::<usize>(); + let count_borders = count_borders(cfg, col, col + span, shape.1); + (width + count_borders, min_width + count_borders) + } + None => (widths[col], min_widths[col]), + }; + + if width >= width_min { + let padding = cfg.get_padding((row, col).into()); + let width = width.saturating_sub(padding.left.size + padding.right.size); + + points.push(((row, col), width)); + } + }); + }); + + points +} + +fn count_borders(cfg: &SpannedConfig, start: usize, end: usize, count_columns: usize) -> usize { + (start..end) + .skip(1) + .filter(|&i| cfg.has_vertical(i, count_columns)) + .count() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn split_test() { + #[cfg(not(feature = "color"))] + let split = |text, width| chunks(text, width).join("\n"); + + #[cfg(feature = "color")] + let split = |text, width| chunks(text, width, "", "").join("\n"); + + assert_eq!(split("123456", 0), ""); + + assert_eq!(split("123456", 1), "1\n2\n3\n4\n5\n6"); + assert_eq!(split("123456", 2), "12\n34\n56"); + assert_eq!(split("12345", 2), "12\n34\n5"); + assert_eq!(split("123456", 6), "123456"); + assert_eq!(split("123456", 10), "123456"); + + assert_eq!(split("π³π³π³π³π³", 1), "οΏ½\nοΏ½\nοΏ½\nοΏ½\nοΏ½"); + assert_eq!(split("π³π³π³π³π³", 2), "π³\nπ³\nπ³\nπ³\nπ³"); + assert_eq!(split("π³π³π³π³π³", 3), "π³οΏ½\nπ³οΏ½\nπ³"); + assert_eq!(split("π³π³π³π³π³", 6), "π³π³π³\nπ³π³"); + assert_eq!(split("π³π³π³π³π³", 20), "π³π³π³π³π³"); + + assert_eq!(split("π³123π³", 1), "οΏ½\n1\n2\n3\nοΏ½"); + assert_eq!(split("π³12π³3", 1), "οΏ½\n1\n2\nοΏ½\n3"); + } + + #[test] + fn chunks_test() { + #[allow(clippy::redundant_closure)] + #[cfg(not(feature = "color"))] + let chunks = |text, width| chunks(text, width); + + #[cfg(feature = "color")] + let chunks = |text, width| chunks(text, width, "", ""); + + assert_eq!(chunks("123456", 0), [""; 0]); + + assert_eq!(chunks("123456", 1), ["1", "2", "3", "4", "5", "6"]); + assert_eq!(chunks("123456", 2), ["12", "34", "56"]); + assert_eq!(chunks("12345", 2), ["12", "34", "5"]); + + assert_eq!(chunks("π³π³π³π³π³", 1), ["οΏ½", "οΏ½", "οΏ½", "οΏ½", "οΏ½"]); + assert_eq!(chunks("π³π³π³π³π³", 2), ["π³", "π³", "π³", "π³", "π³"]); + assert_eq!(chunks("π³π³π³π³π³", 3), ["π³οΏ½", "π³οΏ½", "π³"]); + } + + #[cfg(not(feature = "color"))] + #[test] + fn split_by_line_keeping_words_test() { + let split_keeping_words = |text, width| split_keeping_words(text, width, "\n"); + + assert_eq!(split_keeping_words("123456", 1), "1\n2\n3\n4\n5\n6"); + assert_eq!(split_keeping_words("123456", 2), "12\n34\n56"); + assert_eq!(split_keeping_words("12345", 2), "12\n34\n5 "); + + assert_eq!(split_keeping_words("π³π³π³π³π³", 1), "οΏ½\nοΏ½\nοΏ½\nοΏ½\nοΏ½"); + + assert_eq!(split_keeping_words("111 234 1", 4), "111 \n234 \n1 "); + } + + #[cfg(feature = "color")] + #[test] + fn split_by_line_keeping_words_test() { + #[cfg(feature = "color")] + let split_keeping_words = |text, width| split_keeping_words(text, width, "", ""); + + assert_eq!(split_keeping_words("123456", 1), "1\n2\n3\n4\n5\n6"); + assert_eq!(split_keeping_words("123456", 2), "12\n34\n56"); + assert_eq!(split_keeping_words("12345", 2), "12\n34\n5 "); + + assert_eq!(split_keeping_words("π³π³π³π³π³", 1), "οΏ½\nοΏ½\nοΏ½\nοΏ½\nοΏ½"); + + assert_eq!(split_keeping_words("111 234 1", 4), "111 \n234 \n1 "); + } + + #[cfg(feature = "color")] + #[test] + fn split_by_line_keeping_words_color_test() { + #[cfg(feature = "color")] + let split_keeping_words = |text, width| split_keeping_words(text, width, "", ""); + + #[cfg(not(feature = "color"))] + let split_keeping_words = |text, width| split_keeping_words(text, width, "\n"); + + let text = "\u{1b}[36mJapanese βvacancyβ button\u{1b}[0m"; + + assert_eq!(split_keeping_words(text, 2), "\u{1b}[36mJa\u{1b}[39m\n\u{1b}[36mpa\u{1b}[39m\n\u{1b}[36mne\u{1b}[39m\n\u{1b}[36mse\u{1b}[39m\n\u{1b}[36m β\u{1b}[39m\n\u{1b}[36mva\u{1b}[39m\n\u{1b}[36mca\u{1b}[39m\n\u{1b}[36mnc\u{1b}[39m\n\u{1b}[36myβ\u{1b}[39m\n\u{1b}[36m b\u{1b}[39m\n\u{1b}[36mut\u{1b}[39m\n\u{1b}[36mto\u{1b}[39m\n\u{1b}[36mn\u{1b}[39m "); + assert_eq!(split_keeping_words(text, 1), "\u{1b}[36mJ\u{1b}[39m\n\u{1b}[36ma\u{1b}[39m\n\u{1b}[36mp\u{1b}[39m\n\u{1b}[36ma\u{1b}[39m\n\u{1b}[36mn\u{1b}[39m\n\u{1b}[36me\u{1b}[39m\n\u{1b}[36ms\u{1b}[39m\n\u{1b}[36me\u{1b}[39m\n\u{1b}[36m \u{1b}[39m\n\u{1b}[36mβ\u{1b}[39m\n\u{1b}[36mv\u{1b}[39m\n\u{1b}[36ma\u{1b}[39m\n\u{1b}[36mc\u{1b}[39m\n\u{1b}[36ma\u{1b}[39m\n\u{1b}[36mn\u{1b}[39m\n\u{1b}[36mc\u{1b}[39m\n\u{1b}[36my\u{1b}[39m\n\u{1b}[36mβ\u{1b}[39m\n\u{1b}[36m \u{1b}[39m\n\u{1b}[36mb\u{1b}[39m\n\u{1b}[36mu\u{1b}[39m\n\u{1b}[36mt\u{1b}[39m\n\u{1b}[36mt\u{1b}[39m\n\u{1b}[36mo\u{1b}[39m\n\u{1b}[36mn\u{1b}[39m"); + } + + #[cfg(feature = "color")] + #[test] + fn split_by_line_keeping_words_color_2_test() { + use ansi_str::AnsiStr; + + #[cfg(feature = "color")] + let split_keeping_words = |text, width| split_keeping_words(text, width, "", ""); + + #[cfg(not(feature = "color"))] + let split_keeping_words = |text, width| split_keeping_words(text, width, "\n"); + + let text = "\u{1b}[37mTigre Ecuador OMYA Andina 3824909999 Calcium carbonate Colombia\u{1b}[0m"; + + assert_eq!( + split_keeping_words(text, 2) + .ansi_split("\n") + .collect::<Vec<_>>(), + [ + "\u{1b}[37mTi\u{1b}[39m", + "\u{1b}[37mgr\u{1b}[39m", + "\u{1b}[37me \u{1b}[39m", + "\u{1b}[37mEc\u{1b}[39m", + "\u{1b}[37mua\u{1b}[39m", + "\u{1b}[37mdo\u{1b}[39m", + "\u{1b}[37mr \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37mOM\u{1b}[39m", + "\u{1b}[37mYA\u{1b}[39m", + "\u{1b}[37m A\u{1b}[39m", + "\u{1b}[37mnd\u{1b}[39m", + "\u{1b}[37min\u{1b}[39m", + "\u{1b}[37ma \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m38\u{1b}[39m", + "\u{1b}[37m24\u{1b}[39m", + "\u{1b}[37m90\u{1b}[39m", + "\u{1b}[37m99\u{1b}[39m", + "\u{1b}[37m99\u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37mCa\u{1b}[39m", + "\u{1b}[37mlc\u{1b}[39m", + "\u{1b}[37miu\u{1b}[39m", + "\u{1b}[37mm \u{1b}[39m", + "\u{1b}[37mca\u{1b}[39m", + "\u{1b}[37mrb\u{1b}[39m", + "\u{1b}[37mon\u{1b}[39m", + "\u{1b}[37mat\u{1b}[39m", + "\u{1b}[37me \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37mCo\u{1b}[39m", + "\u{1b}[37mlo\u{1b}[39m", + "\u{1b}[37mmb\u{1b}[39m", + "\u{1b}[37mia\u{1b}[39m" + ] + ); + + assert_eq!( + split_keeping_words(text, 1) + .ansi_split("\n") + .collect::<Vec<_>>(), + [ + "\u{1b}[37mT\u{1b}[39m", + "\u{1b}[37mi\u{1b}[39m", + "\u{1b}[37mg\u{1b}[39m", + "\u{1b}[37mr\u{1b}[39m", + "\u{1b}[37me\u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37mE\u{1b}[39m", + "\u{1b}[37mc\u{1b}[39m", + "\u{1b}[37mu\u{1b}[39m", + "\u{1b}[37ma\u{1b}[39m", + "\u{1b}[37md\u{1b}[39m", + "\u{1b}[37mo\u{1b}[39m", + "\u{1b}[37mr\u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37mO\u{1b}[39m", + "\u{1b}[37mM\u{1b}[39m", + "\u{1b}[37mY\u{1b}[39m", + "\u{1b}[37mA\u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37mA\u{1b}[39m", + "\u{1b}[37mn\u{1b}[39m", + "\u{1b}[37md\u{1b}[39m", + "\u{1b}[37mi\u{1b}[39m", + "\u{1b}[37mn\u{1b}[39m", + "\u{1b}[37ma\u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m3\u{1b}[39m", + "\u{1b}[37m8\u{1b}[39m", + "\u{1b}[37m2\u{1b}[39m", + "\u{1b}[37m4\u{1b}[39m", + "\u{1b}[37m9\u{1b}[39m", + "\u{1b}[37m0\u{1b}[39m", + "\u{1b}[37m9\u{1b}[39m", + "\u{1b}[37m9\u{1b}[39m", + "\u{1b}[37m9\u{1b}[39m", + "\u{1b}[37m9\u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37mC\u{1b}[39m", + "\u{1b}[37ma\u{1b}[39m", + "\u{1b}[37ml\u{1b}[39m", + "\u{1b}[37mc\u{1b}[39m", + "\u{1b}[37mi\u{1b}[39m", + "\u{1b}[37mu\u{1b}[39m", + "\u{1b}[37mm\u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37mc\u{1b}[39m", + "\u{1b}[37ma\u{1b}[39m", + "\u{1b}[37mr\u{1b}[39m", + "\u{1b}[37mb\u{1b}[39m", + "\u{1b}[37mo\u{1b}[39m", + "\u{1b}[37mn\u{1b}[39m", + "\u{1b}[37ma\u{1b}[39m", + "\u{1b}[37mt\u{1b}[39m", + "\u{1b}[37me\u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37m \u{1b}[39m", + "\u{1b}[37mC\u{1b}[39m", + "\u{1b}[37mo\u{1b}[39m", + "\u{1b}[37ml\u{1b}[39m", + "\u{1b}[37mo\u{1b}[39m", + "\u{1b}[37mm\u{1b}[39m", + "\u{1b}[37mb\u{1b}[39m", + "\u{1b}[37mi\u{1b}[39m", + "\u{1b}[37ma\u{1b}[39m" + ] + ) + } + + #[cfg(feature = "color")] + #[test] + fn split_by_line_keeping_words_color_3_test() { + let split = |text, width| split_keeping_words(text, width, "", ""); + assert_eq!( + split( + "\u{1b}[37mπ΅π»π΅π»π΅π»π΅π»π΅π»π΅π»π΅π»π΅π»π΅π»π΅π»\u{1b}[0m", + 3, + ), + "\u{1b}[37mπ΅οΏ½\u{1b}[39m\n\u{1b}[37mπ΅οΏ½\u{1b}[39m\n\u{1b}[37mπ΅οΏ½\u{1b}[39m\n\u{1b}[37mπ΅οΏ½\u{1b}[39m\n\u{1b}[37mπ΅οΏ½\u{1b}[39m\n\u{1b}[37mπ΅οΏ½\u{1b}[39m\n\u{1b}[37mπ΅οΏ½\u{1b}[39m\n\u{1b}[37mπ΅οΏ½\u{1b}[39m\n\u{1b}[37mπ΅οΏ½\u{1b}[39m\n\u{1b}[37mπ΅οΏ½\u{1b}[39m", + ); + assert_eq!( + split("\u{1b}[37mthis is a long sentence\u{1b}[0m", 7), + "\u{1b}[37mthis is\u{1b}[39m\n\u{1b}[37m a long\u{1b}[39m\n\u{1b}[37m senten\u{1b}[39m\n\u{1b}[37mce\u{1b}[39m " + ); + assert_eq!( + split("\u{1b}[37mHello World\u{1b}[0m", 7), + "\u{1b}[37mHello \u{1b}[39m \n\u{1b}[37mWorld\u{1b}[39m " + ); + assert_eq!( + split("\u{1b}[37mHello Wo\u{1b}[37mrld\u{1b}[0m", 7), + "\u{1b}[37mHello \u{1b}[39m \n\u{1b}[37mWo\u{1b}[39m\u{1b}[37mrld\u{1b}[39m " + ); + assert_eq!( + split("\u{1b}[37mHello Wo\u{1b}[37mrld\u{1b}[0m", 8), + "\u{1b}[37mHello \u{1b}[39m \n\u{1b}[37mWo\u{1b}[39m\u{1b}[37mrld\u{1b}[39m " + ); + } + + #[cfg(not(feature = "color"))] + #[test] + fn split_keeping_words_4_test() { + let split_keeping_words = |text, width| split_keeping_words(text, width, "\n"); + + assert_eq!(split_keeping_words("12345678", 3,), "123\n456\n78 "); + assert_eq!(split_keeping_words("12345678", 2,), "12\n34\n56\n78"); + } + + #[cfg(feature = "color")] + #[test] + fn split_keeping_words_4_test() { + let split_keeping_words = |text, width| split_keeping_words(text, width, "", ""); + + #[cfg(not(feature = "color"))] + let split_keeping_words = |text, width| split_keeping_words(text, width, "\n"); + + assert_eq!(split_keeping_words("12345678", 3,), "123\n456\n78 "); + assert_eq!(split_keeping_words("12345678", 2,), "12\n34\n56\n78"); + } + + #[cfg(feature = "color")] + #[test] + fn chunks_test_with_prefix_and_suffix() { + assert_eq!(chunks("123456", 0, "^", "$"), ["^$"; 0]); + + assert_eq!( + chunks("123456", 1, "^", "$"), + ["^1$", "^2$", "^3$", "^4$", "^5$", "^6$"] + ); + assert_eq!(chunks("123456", 2, "^", "$"), ["^12$", "^34$", "^56$"]); + assert_eq!(chunks("12345", 2, "^", "$"), ["^12$", "^34$", "^5$"]); + + assert_eq!( + chunks("π³π³π³π³π³", 1, "^", "$"), + ["^οΏ½$", "^οΏ½$", "^οΏ½$", "^οΏ½$", "^οΏ½$"] + ); + assert_eq!( + chunks("π³π³π³π³π³", 2, "^", "$"), + ["^π³$", "^π³$", "^π³$", "^π³$", "^π³$"] + ); + assert_eq!( + chunks("π³π³π³π³π³", 3, "^", "$"), + ["^π³οΏ½$", "^π³οΏ½$", "^π³$"] + ); + } + + #[cfg(feature = "color")] + #[test] + fn split_by_line_keeping_words_test_with_prefix_and_suffix() { + assert_eq!( + split_keeping_words("123456", 1, "^", "$"), + "^1$\n^2$\n^3$\n^4$\n^5$\n^6$" + ); + assert_eq!( + split_keeping_words("123456", 2, "^", "$"), + "^12$\n^34$\n^56$" + ); + assert_eq!( + split_keeping_words("12345", 2, "^", "$"), + "^12$\n^34$\n^5$ " + ); + + assert_eq!( + split_keeping_words("π³π³π³π³π³", 1, "^", "$"), + "^οΏ½$\n^οΏ½$\n^οΏ½$\n^οΏ½$\n^οΏ½$" + ); + } + + #[cfg(feature = "color")] + #[test] + fn split_by_line_keeping_words_color_2_test_with_prefix_and_suffix() { + use ansi_str::AnsiStr; + + let text = "\u{1b}[37mTigre Ecuador OMYA Andina 3824909999 Calcium carbonate Colombia\u{1b}[0m"; + + assert_eq!( + split_keeping_words(text, 2, "^", "$") + .ansi_split("\n") + .collect::<Vec<_>>(), + [ + "^\u{1b}[37mTi\u{1b}[39m$", + "^\u{1b}[37mgr\u{1b}[39m$", + "^\u{1b}[37me \u{1b}[39m$", + "^\u{1b}[37mEc\u{1b}[39m$", + "^\u{1b}[37mua\u{1b}[39m$", + "^\u{1b}[37mdo\u{1b}[39m$", + "^\u{1b}[37mr \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37mOM\u{1b}[39m$", + "^\u{1b}[37mYA\u{1b}[39m$", + "^\u{1b}[37m A\u{1b}[39m$", + "^\u{1b}[37mnd\u{1b}[39m$", + "^\u{1b}[37min\u{1b}[39m$", + "^\u{1b}[37ma \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m38\u{1b}[39m$", + "^\u{1b}[37m24\u{1b}[39m$", + "^\u{1b}[37m90\u{1b}[39m$", + "^\u{1b}[37m99\u{1b}[39m$", + "^\u{1b}[37m99\u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37mCa\u{1b}[39m$", + "^\u{1b}[37mlc\u{1b}[39m$", + "^\u{1b}[37miu\u{1b}[39m$", + "^\u{1b}[37mm \u{1b}[39m$", + "^\u{1b}[37mca\u{1b}[39m$", + "^\u{1b}[37mrb\u{1b}[39m$", + "^\u{1b}[37mon\u{1b}[39m$", + "^\u{1b}[37mat\u{1b}[39m$", + "^\u{1b}[37me \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37mCo\u{1b}[39m$", + "^\u{1b}[37mlo\u{1b}[39m$", + "^\u{1b}[37mmb\u{1b}[39m$", + "^\u{1b}[37mia\u{1b}[39m$" + ] + ); + + assert_eq!( + split_keeping_words(text, 1, "^", "$") + .ansi_split("\n") + .collect::<Vec<_>>(), + [ + "^\u{1b}[37mT\u{1b}[39m$", + "^\u{1b}[37mi\u{1b}[39m$", + "^\u{1b}[37mg\u{1b}[39m$", + "^\u{1b}[37mr\u{1b}[39m$", + "^\u{1b}[37me\u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37mE\u{1b}[39m$", + "^\u{1b}[37mc\u{1b}[39m$", + "^\u{1b}[37mu\u{1b}[39m$", + "^\u{1b}[37ma\u{1b}[39m$", + "^\u{1b}[37md\u{1b}[39m$", + "^\u{1b}[37mo\u{1b}[39m$", + "^\u{1b}[37mr\u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37mO\u{1b}[39m$", + "^\u{1b}[37mM\u{1b}[39m$", + "^\u{1b}[37mY\u{1b}[39m$", + "^\u{1b}[37mA\u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37mA\u{1b}[39m$", + "^\u{1b}[37mn\u{1b}[39m$", + "^\u{1b}[37md\u{1b}[39m$", + "^\u{1b}[37mi\u{1b}[39m$", + "^\u{1b}[37mn\u{1b}[39m$", + "^\u{1b}[37ma\u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m3\u{1b}[39m$", + "^\u{1b}[37m8\u{1b}[39m$", + "^\u{1b}[37m2\u{1b}[39m$", + "^\u{1b}[37m4\u{1b}[39m$", + "^\u{1b}[37m9\u{1b}[39m$", + "^\u{1b}[37m0\u{1b}[39m$", + "^\u{1b}[37m9\u{1b}[39m$", + "^\u{1b}[37m9\u{1b}[39m$", + "^\u{1b}[37m9\u{1b}[39m$", + "^\u{1b}[37m9\u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37mC\u{1b}[39m$", + "^\u{1b}[37ma\u{1b}[39m$", + "^\u{1b}[37ml\u{1b}[39m$", + "^\u{1b}[37mc\u{1b}[39m$", + "^\u{1b}[37mi\u{1b}[39m$", + "^\u{1b}[37mu\u{1b}[39m$", + "^\u{1b}[37mm\u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37mc\u{1b}[39m$", + "^\u{1b}[37ma\u{1b}[39m$", + "^\u{1b}[37mr\u{1b}[39m$", + "^\u{1b}[37mb\u{1b}[39m$", + "^\u{1b}[37mo\u{1b}[39m$", + "^\u{1b}[37mn\u{1b}[39m$", + "^\u{1b}[37ma\u{1b}[39m$", + "^\u{1b}[37mt\u{1b}[39m$", + "^\u{1b}[37me\u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37m \u{1b}[39m$", + "^\u{1b}[37mC\u{1b}[39m$", + "^\u{1b}[37mo\u{1b}[39m$", + "^\u{1b}[37ml\u{1b}[39m$", + "^\u{1b}[37mo\u{1b}[39m$", + "^\u{1b}[37mm\u{1b}[39m$", + "^\u{1b}[37mb\u{1b}[39m$", + "^\u{1b}[37mi\u{1b}[39m$", + "^\u{1b}[37ma\u{1b}[39m$" + ] + ) + } + + #[cfg(feature = "color")] + #[test] + fn chunks_wrap_2() { + let text = "\u{1b}[30mDebian\u{1b}[0m\u{1b}[31mDebian\u{1b}[0m\u{1b}[32mDebian\u{1b}[0m\u{1b}[33mDebian\u{1b}[0m\u{1b}[34mDebian\u{1b}[0m\u{1b}[35mDebian\u{1b}[0m\u{1b}[36mDebian\u{1b}[0m\u{1b}[37mDebian\u{1b}[0m\u{1b}[40mDebian\u{1b}[0m\u{1b}[41mDebian\u{1b}[0m\u{1b}[42mDebian\u{1b}[0m\u{1b}[43mDebian\u{1b}[0m\u{1b}[44mDebian\u{1b}[0m"; + assert_eq!( + chunks(text, 30, "", ""), + [ + "\u{1b}[30mDebian\u{1b}[39m\u{1b}[31mDebian\u{1b}[39m\u{1b}[32mDebian\u{1b}[39m\u{1b}[33mDebian\u{1b}[39m\u{1b}[34mDebian\u{1b}[39m", + "\u{1b}[35mDebian\u{1b}[39m\u{1b}[36mDebian\u{1b}[39m\u{1b}[37mDebian\u{1b}[39m\u{1b}[40mDebian\u{1b}[49m\u{1b}[41mDebian\u{1b}[49m", + "\u{1b}[42mDebian\u{1b}[49m\u{1b}[43mDebian\u{1b}[49m\u{1b}[44mDebian\u{1b}[49m", + ] + ); + } + + #[cfg(feature = "color")] + #[test] + fn chunks_wrap_3() { + let text = "\u{1b}[37mCreate bytes from the \u{1b}[0m\u{1b}[7;34marg\u{1b}[0m\u{1b}[37muments.\u{1b}[0m"; + + assert_eq!( + chunks(text, 22, "", ""), + [ + "\u{1b}[37mCreate bytes from the \u{1b}[39m", + "\u{1b}[7m\u{1b}[34marg\u{1b}[27m\u{1b}[39m\u{1b}[37muments.\u{1b}[39m" + ] + ); + } + + #[cfg(feature = "color")] + #[test] + fn chunks_wrap_3_keeping_words() { + let text = "\u{1b}[37mCreate bytes from the \u{1b}[0m\u{1b}[7;34marg\u{1b}[0m\u{1b}[37muments.\u{1b}[0m"; + + assert_eq!( + split_keeping_words(text, 22, "", ""), + "\u{1b}[37mCreate bytes from the \u{1b}[39m\n\u{1b}[7m\u{1b}[34marg\u{1b}[27m\u{1b}[39m\u{1b}[37muments.\u{1b}[39m " + ); + } + + #[cfg(feature = "color")] + #[test] + fn chunks_wrap_4() { + let text = "\u{1b}[37mReturns the floor of a number (l\u{1b}[0m\u{1b}[41;37marg\u{1b}[0m\u{1b}[37mest integer less than or equal to that number).\u{1b}[0m"; + + assert_eq!( + chunks(text, 10, "", ""), + [ + "\u{1b}[37mReturns th\u{1b}[39m", + "\u{1b}[37me floor of\u{1b}[39m", + "\u{1b}[37m a number \u{1b}[39m", + "\u{1b}[37m(l\u{1b}[39m\u{1b}[37m\u{1b}[41marg\u{1b}[39m\u{1b}[49m\u{1b}[37mest i\u{1b}[39m", + "\u{1b}[37mnteger les\u{1b}[39m", + "\u{1b}[37ms than or \u{1b}[39m", + "\u{1b}[37mequal to t\u{1b}[39m", + "\u{1b}[37mhat number\u{1b}[39m", + "\u{1b}[37m).\u{1b}[39m", + ] + ); + } + + #[cfg(feature = "color")] + #[test] + fn chunks_wrap_4_keeping_words() { + let text = "\u{1b}[37mReturns the floor of a number (l\u{1b}[0m\u{1b}[41;37marg\u{1b}[0m\u{1b}[37mest integer less than or equal to that number).\u{1b}[0m"; + assert_eq!( + split_keeping_words(text, 10, "", ""), + concat!( + "\u{1b}[37mReturns \u{1b}[39m \n", + "\u{1b}[37mthe floor \u{1b}[39m\n", + "\u{1b}[37mof a \u{1b}[39m \n", + "\u{1b}[37mnumber \u{1b}[39m \n", + "\u{1b}[37m(l\u{1b}[39m\u{1b}[37m\u{1b}[41marg\u{1b}[39m\u{1b}[49m\u{1b}[37mest \u{1b}[39m \n", + "\u{1b}[37minteger \u{1b}[39m \n", + "\u{1b}[37mless than \u{1b}[39m\n", + "\u{1b}[37mor equal \u{1b}[39m \n", + "\u{1b}[37mto that \u{1b}[39m \n", + "\u{1b}[37mnumber).\u{1b}[39m ", + ) + ); + } +} + +// \u{1b}[37mReturns \u{1b}[39m\n +// \u{1b}[37mthe floor \u{1b}[39m\n +// \u{1b}[37mof a \u{1b}[39m\n +// \u{1b}[37mnumber \u{1b}[39m\u{1b}[49m\n +// \u{1b}[37m\u{1b}[41m(l\u{1b}[39m\u{1b}[37m\u{1b}[41marg\u{1b}[39m\u{1b}[49m\u{1b}[37mest \u{1b}[39m\n +// \u{1b}[37minteger \u{1b}[39m\n +// \u{1b}[37mless than \u{1b}[39m\n +// \u{1b}[37mor equal \u{1b}[39m\n +// \u{1b}[37mto that \u{1b}[39m\n +// \u{1b}[37mnumber).\u{1b}[39m " + +// +// + +// \u{1b}[37mReturns \u{1b}[39m\n +// \u{1b}[37mthe floor \u{1b}[39m\n +// \u{1b}[37mof a \u{1b}[39m\n +// \u{1b}[37mnumber \u{1b}[39m\u{1b}[49m\n +// \u{1b}[37m\u{1b}[41m(l\u{1b}[39m\u{1b}[37m\u{1b}[41marg\u{1b}[39m\u{1b}[49m\u{1b}[37mest \u{1b}[39m\n +// \u{1b}[37minteger \u{1b}[39m\n +// \u{1b}[37mless than \u{1b}[39m\n +// \u{1b}[37mor equal \u{1b}[39m\n +// \u{1b}[37mto that \u{1b}[39m\n +// \u{1b}[37mnumber).\u{1b}[39m " + +// "\u{1b}[37mReturns\u{1b}[37m \u{1b}[39m\n +// \u{1b}[37mthe\u{1b}[37m floor\u{1b}[37m \u{1b}[39m\n +// \u{1b}[37mof\u{1b}[37m a\u{1b}[37m \u{1b}[39m\n +// \u{1b}[37mnumber\u{1b}[37m \u{1b}[39m\u{1b}[49m\n +// \u{1b}[37m\u{1b}[41m(l\u{1b}[39m\u{1b}[37m\u{1b}[41marg\u{1b}[39m\u{1b}[49m\u{1b}[37mest\u{1b}[37m \u{1b}[39m\n +// \u{1b}[37minteger\u{1b}[37m \u{1b}[39m\n +// \u{1b}[37mless\u{1b}[37m than\u{1b}[37m \u{1b}[39m\n +// \u{1b}[37mor\u{1b}[37m equal\u{1b}[37m \u{1b}[39m\n +// \u{1b}[37mto\u{1b}[37m that\u{1b}[37m \u{1b}[39m\n +// \u{1b}[37mnumber).\u{1b}[39m " |