diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-07 05:48:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-07 05:48:48 +0000 |
commit | ef24de24a82fe681581cc130f342363c47c0969a (patch) | |
tree | 0d494f7e1a38b95c92426f58fe6eaa877303a86c /vendor/tabled/src/tables | |
parent | Releasing progress-linux version 1.74.1+dfsg1-1~progress7.99u1. (diff) | |
download | rustc-ef24de24a82fe681581cc130f342363c47c0969a.tar.xz rustc-ef24de24a82fe681581cc130f342363c47c0969a.zip |
Merging upstream version 1.75.0+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/tabled/src/tables')
-rw-r--r-- | vendor/tabled/src/tables/compact.rs | 309 | ||||
-rw-r--r-- | vendor/tabled/src/tables/extended.rs | 338 | ||||
-rw-r--r-- | vendor/tabled/src/tables/iter.rs | 344 | ||||
-rw-r--r-- | vendor/tabled/src/tables/mod.rs | 48 | ||||
-rw-r--r-- | vendor/tabled/src/tables/table.rs | 464 | ||||
-rw-r--r-- | vendor/tabled/src/tables/table_pool.rs | 1607 | ||||
-rw-r--r-- | vendor/tabled/src/tables/util/mod.rs | 2 | ||||
-rw-r--r-- | vendor/tabled/src/tables/util/utf8_writer.rs | 29 |
8 files changed, 3141 insertions, 0 deletions
diff --git a/vendor/tabled/src/tables/compact.rs b/vendor/tabled/src/tables/compact.rs new file mode 100644 index 000000000..14c1e2b5b --- /dev/null +++ b/vendor/tabled/src/tables/compact.rs @@ -0,0 +1,309 @@ +//! This module contains a [`CompactTable`] table. +//! +//! In contrast to [`Table`] [`CompactTable`] does no allocations but it consumes an iterator. +//! It's useful when you don't want to re/allocate a buffer for your data. +//! +//! # Example +//! +//! It works smoothly with arrays. +//! +#![cfg_attr(feature = "std", doc = "```")] +#![cfg_attr(not(feature = "std"), doc = "```ignore")] +//!use tabled::{settings::Style, tables::CompactTable}; +//! +//! let data = [ +//! ["FreeBSD", "1993", "William and Lynne Jolitz", "?"], +//! ["OpenBSD", "1995", "Theo de Raadt", ""], +//! ["HardenedBSD", "2014", "Oliver Pinter and Shawn Webb", ""], +//! ]; +//! +//! let table = CompactTable::from(data) +//! .with(Style::psql()) +//! .to_string(); +//! +//! assert_eq!( +//! table, +//! concat!( +//! " FreeBSD | 1993 | William and Lynne Jolitz | ? \n", +//! "-------------+------+------------------------------+---\n", +//! " OpenBSD | 1995 | Theo de Raadt | \n", +//! " HardenedBSD | 2014 | Oliver Pinter and Shawn Webb | ", +//! ) +//! ); +//! ``` +//! +//! But it's default creation requires to be given an estimated cell width, and the amount of columns. +//! +#![cfg_attr(feature = "std", doc = "```")] +#![cfg_attr(not(feature = "std"), doc = "```ignore")] +//!use tabled::{settings::Style, tables::CompactTable}; +//! +//! let data = [ +//! ["FreeBSD", "1993", "William and Lynne Jolitz", "?"], +//! ["OpenBSD", "1995", "Theo de Raadt", ""], +//! ["HardenedBSD", "2014", "Oliver Pinter and Shawn Webb", ""], +//! ]; +//! +//! // See what will happen if the given width is too narrow +//! +//! let table = CompactTable::new(&data) +//! .columns(4) +//! .width(5) +//! .with(Style::ascii()) +//! .to_string(); +//! +//! assert_eq!( +//! table, +//! "+-----+-----+-----+-----+\n\ +//! | FreeBSD | 1993 | William and Lynne Jolitz | ? |\n\ +//! |-----+-----+-----+-----|\n\ +//! | OpenBSD | 1995 | Theo de Raadt | |\n\ +//! |-----+-----+-----+-----|\n\ +//! | HardenedBSD | 2014 | Oliver Pinter and Shawn Webb | |\n\ +//! +-----+-----+-----+-----+" +//! ); +//! ``` +//! +//! [`Table`]: crate::Table + +use core::cmp::max; +use core::fmt; + +use crate::{ + grid::{ + config::{AlignmentHorizontal, CompactConfig, Indent, Sides}, + dimension::{ConstDimension, ConstSize, Dimension}, + records::{ + into_records::{LimitColumns, LimitRows}, + IntoRecords, IterRecords, + }, + util::string::string_width, + CompactGrid, + }, + settings::{Style, TableOption}, +}; + +/// A table which consumes an [`IntoRecords`] iterator. +/// It assumes that the content has only single line. +#[derive(Debug, Clone)] +pub struct CompactTable<I, D> { + records: I, + cfg: CompactConfig, + dims: D, + count_columns: usize, + count_rows: Option<usize>, +} + +impl<I> CompactTable<I, ConstDimension<0, 0>> { + /// Creates a new [`CompactTable`] structure with a width dimension for all columns. + pub const fn new(iter: I) -> Self + where + I: IntoRecords, + { + Self { + records: iter, + cfg: create_config(), + count_columns: 0, + count_rows: None, + dims: ConstDimension::new(ConstSize::Value(2), ConstSize::Value(1)), + } + } +} + +impl<I, const ROWS: usize, const COLS: usize> CompactTable<I, ConstDimension<COLS, ROWS>> { + /// Set a height for each row. + pub fn height<S: Into<ConstSize<COUNT_ROWS>>, const COUNT_ROWS: usize>( + self, + size: S, + ) -> CompactTable<I, ConstDimension<COLS, COUNT_ROWS>> { + let (width, _) = self.dims.into(); + CompactTable { + dims: ConstDimension::new(width, size.into()), + records: self.records, + cfg: self.cfg, + count_columns: self.count_columns, + count_rows: self.count_rows, + } + } + + /// Set a width for each column. + pub fn width<S: Into<ConstSize<COUNT_COLUMNS>>, const COUNT_COLUMNS: usize>( + self, + size: S, + ) -> CompactTable<I, ConstDimension<COUNT_COLUMNS, ROWS>> { + let (_, height) = self.dims.into(); + CompactTable { + dims: ConstDimension::new(size.into(), height), + records: self.records, + cfg: self.cfg, + count_columns: self.count_columns, + count_rows: self.count_rows, + } + } +} + +impl<I, D> CompactTable<I, D> { + /// Creates a new [`CompactTable`] structure with a known dimension. + /// + /// Notice that the function wont call [`Estimate`]. + /// + /// [`Estimate`]: crate::grid::dimension::Estimate + pub fn with_dimension(iter: I, dimension: D) -> Self + where + I: IntoRecords, + { + Self { + records: iter, + dims: dimension, + cfg: create_config(), + count_columns: 0, + count_rows: None, + } + } + + /// With is a generic function which applies options to the [`CompactTable`]. + pub fn with<O>(mut self, option: O) -> Self + where + for<'a> O: TableOption<IterRecords<&'a I>, D, CompactConfig>, + { + let mut records = IterRecords::new(&self.records, self.count_columns, self.count_rows); + option.change(&mut records, &mut self.cfg, &mut self.dims); + + self + } + + /// Limit a number of rows. + pub const fn rows(mut self, count_rows: usize) -> Self { + self.count_rows = Some(count_rows); + self + } + + /// Limit a number of columns. + pub const fn columns(mut self, count: usize) -> Self { + self.count_columns = count; + self + } + + /// Returns a table config. + pub fn get_config(&self) -> &CompactConfig { + &self.cfg + } + + /// Returns a table config. + pub fn get_config_mut(&mut self) -> &mut CompactConfig { + &mut self.cfg + } + + /// Format table into [fmt::Write]er. + pub fn fmt<W>(self, writer: W) -> fmt::Result + where + I: IntoRecords, + D: Dimension, + W: fmt::Write, + { + build_grid( + writer, + self.records, + self.dims, + self.cfg, + self.count_columns, + self.count_rows, + ) + } + + /// Format table into a writer. + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + pub fn build<W>(self, writer: W) -> std::io::Result<()> + where + I: IntoRecords, + D: Dimension, + W: std::io::Write, + { + let writer = super::util::utf8_writer::UTF8Writer::new(writer); + self.fmt(writer) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) + } + + /// Build a string. + /// + /// We can't implement [`std::string::ToString`] cause it does takes `&self` reference. + #[allow(clippy::inherent_to_string)] + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + pub fn to_string(self) -> String + where + I: IntoRecords, + D: Dimension, + { + let mut buf = String::new(); + self.fmt(&mut buf).unwrap(); + buf + } +} + +impl<T, const ROWS: usize, const COLS: usize> From<[[T; COLS]; ROWS]> + for CompactTable<[[T; COLS]; ROWS], ConstDimension<COLS, ROWS>> +where + T: AsRef<str>, +{ + fn from(mat: [[T; COLS]; ROWS]) -> Self { + let mut width = [0; COLS]; + for row in mat.iter() { + for (col, text) in row.iter().enumerate() { + let text = text.as_ref(); + let text_width = string_width(text); + width[col] = max(width[col], text_width); + } + } + + // add padding + for w in &mut width { + *w += 2; + } + + let dims = ConstDimension::new(ConstSize::List(width), ConstSize::Value(1)); + Self::with_dimension(mat, dims).columns(COLS).rows(ROWS) + } +} + +fn build_grid<W: fmt::Write, I: IntoRecords, D: Dimension>( + writer: W, + records: I, + dims: D, + config: CompactConfig, + cols: usize, + rows: Option<usize>, +) -> Result<(), fmt::Error> { + match rows { + Some(limit) => { + let records = LimitRows::new(records, limit); + let records = LimitColumns::new(records, cols); + let records = IterRecords::new(records, cols, rows); + CompactGrid::new(records, dims, config).build(writer) + } + None => { + let records = LimitColumns::new(records, cols); + let records = IterRecords::new(records, cols, rows); + CompactGrid::new(records, dims, config).build(writer) + } + } +} + +const fn create_config() -> CompactConfig { + CompactConfig::empty() + .set_padding(Sides::new( + Indent::spaced(1), + Indent::spaced(1), + Indent::zero(), + Indent::zero(), + )) + .set_alignment_horizontal(AlignmentHorizontal::Left) + .set_borders(*Style::ascii().get_borders()) +} + +impl<R, D> TableOption<R, D, CompactConfig> for CompactConfig { + fn change(self, _: &mut R, cfg: &mut CompactConfig, _: &mut D) { + *cfg = self; + } +} diff --git a/vendor/tabled/src/tables/extended.rs b/vendor/tabled/src/tables/extended.rs new file mode 100644 index 000000000..478505c4b --- /dev/null +++ b/vendor/tabled/src/tables/extended.rs @@ -0,0 +1,338 @@ +//! This module contains an [`ExtendedTable`] structure which is useful in cases where +//! a structure has a lot of fields. +//! +#![cfg_attr(feature = "derive", doc = "```")] +#![cfg_attr(not(feature = "derive"), doc = "```ignore")] +//! use tabled::{Tabled, tables::ExtendedTable}; +//! +//! #[derive(Tabled)] +//! struct Language { +//! name: &'static str, +//! designed_by: &'static str, +//! invented_year: usize, +//! } +//! +//! let languages = vec![ +//! Language{ +//! name: "C", +//! designed_by: "Dennis Ritchie", +//! invented_year: 1972 +//! }, +//! Language{ +//! name: "Rust", +//! designed_by: "Graydon Hoare", +//! invented_year: 2010 +//! }, +//! Language{ +//! name: "Go", +//! designed_by: "Rob Pike", +//! invented_year: 2009 +//! }, +//! ]; +//! +//! let table = ExtendedTable::new(languages).to_string(); +//! +//! let expected = "-[ RECORD 0 ]-+---------------\n\ +//! name | C\n\ +//! designed_by | Dennis Ritchie\n\ +//! invented_year | 1972\n\ +//! -[ RECORD 1 ]-+---------------\n\ +//! name | Rust\n\ +//! designed_by | Graydon Hoare\n\ +//! invented_year | 2010\n\ +//! -[ RECORD 2 ]-+---------------\n\ +//! name | Go\n\ +//! designed_by | Rob Pike\n\ +//! invented_year | 2009"; +//! +//! assert_eq!(table, expected); +//! ``` + +use std::borrow::Cow; +use std::fmt::{self, Display}; + +use crate::grid::util::string::string_width; +use crate::Tabled; + +/// `ExtendedTable` display data in a 'expanded display mode' from postgresql. +/// It may be useful for a large data sets with a lot of fields. +/// +/// See 'Examples' in <https://www.postgresql.org/docs/current/app-psql.html>. +/// +/// It escapes strings to resolve a multi-line ones. +/// Because of that ANSI sequences will be not be rendered too so colores will not be showed. +/// +/// ``` +/// use tabled::tables::ExtendedTable; +/// +/// let data = vec!["Hello", "2021"]; +/// let table = ExtendedTable::new(&data).to_string(); +/// +/// assert_eq!( +/// table, +/// concat!( +/// "-[ RECORD 0 ]-\n", +/// "&str | Hello\n", +/// "-[ RECORD 1 ]-\n", +/// "&str | 2021", +/// ) +/// ); +/// ``` +#[derive(Debug, Clone)] +pub struct ExtendedTable { + fields: Vec<String>, + records: Vec<Vec<String>>, +} + +impl ExtendedTable { + /// Creates a new instance of `ExtendedTable` + pub fn new<T>(iter: impl IntoIterator<Item = T>) -> Self + where + T: Tabled, + { + let data = iter + .into_iter() + .map(|i| { + i.fields() + .into_iter() + .map(|s| s.escape_debug().to_string()) + .collect() + }) + .collect(); + let header = T::headers() + .into_iter() + .map(|s| s.escape_debug().to_string()) + .collect(); + + Self { + records: data, + fields: header, + } + } + + /// Truncates table to a set width value for a table. + /// It returns a success inticator, where `false` means it's not possible to set the table width, + /// because of the given arguments. + /// + /// It tries to not affect fields, but if there's no enough space all records will be deleted and fields will be cut. + /// + /// The minimum width is 14. + pub fn truncate(&mut self, max: usize, suffix: &str) -> bool { + // -[ RECORD 0 ]- + let teplate_width = self.records.len().to_string().len() + 13; + let min_width = teplate_width; + if max < min_width { + return false; + } + + let suffix_width = string_width(suffix); + if max < suffix_width { + return false; + } + + let max = max - suffix_width; + + let fields_max_width = self + .fields + .iter() + .map(|s| string_width(s)) + .max() + .unwrap_or_default(); + + // 3 is a space for ' | ' + let fields_affected = max < fields_max_width + 3; + if fields_affected { + if max < 3 { + return false; + } + + let max = max - 3; + + if max < suffix_width { + return false; + } + + let max = max - suffix_width; + + truncate_fields(&mut self.fields, max, suffix); + truncate_records(&mut self.records, 0, suffix); + } else { + let max = max - fields_max_width - 3 - suffix_width; + truncate_records(&mut self.records, max, suffix); + } + + true + } +} + +impl From<Vec<Vec<String>>> for ExtendedTable { + fn from(mut data: Vec<Vec<String>>) -> Self { + if data.is_empty() { + return Self { + fields: vec![], + records: vec![], + }; + } + + let fields = data.remove(0); + + Self { + fields, + records: data, + } + } +} + +impl Display for ExtendedTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.records.is_empty() { + return Ok(()); + } + + // It's possible that field|header can be a multiline string so + // we escape it and trim \" chars. + let fields = self.fields.iter().collect::<Vec<_>>(); + + let max_field_width = fields + .iter() + .map(|s| string_width(s)) + .max() + .unwrap_or_default(); + + let max_values_length = self + .records + .iter() + .map(|record| record.iter().map(|s| string_width(s)).max()) + .max() + .unwrap_or_default() + .unwrap_or_default(); + + for (i, records) in self.records.iter().enumerate() { + write_header_template(f, i, max_field_width, max_values_length)?; + + for (value, field) in records.iter().zip(fields.iter()) { + writeln!(f)?; + write_record(f, field, value, max_field_width)?; + } + + let is_last_record = i + 1 == self.records.len(); + if !is_last_record { + writeln!(f)?; + } + } + + Ok(()) + } +} + +fn truncate_records(records: &mut Vec<Vec<String>>, max_width: usize, suffix: &str) { + for fields in records { + truncate_fields(fields, max_width, suffix); + } +} + +fn truncate_fields(records: &mut Vec<String>, max_width: usize, suffix: &str) { + for text in records { + truncate(text, max_width, suffix); + } +} + +fn write_header_template( + f: &mut fmt::Formatter<'_>, + index: usize, + max_field_width: usize, + max_values_length: usize, +) -> fmt::Result { + let mut template = format!("-[ RECORD {index} ]-"); + let default_template_length = template.len(); + + // 3 - is responsible for ' | ' formatting + let max_line_width = std::cmp::max( + max_field_width + 3 + max_values_length, + default_template_length, + ); + let rest_to_print = max_line_width - default_template_length; + if rest_to_print > 0 { + // + 1 is a space after field name and we get a next pos so its +2 + if max_field_width + 2 > default_template_length { + let part1 = (max_field_width + 1) - default_template_length; + let part2 = rest_to_print - part1 - 1; + + template.extend( + std::iter::repeat('-') + .take(part1) + .chain(std::iter::once('+')) + .chain(std::iter::repeat('-').take(part2)), + ); + } else { + template.extend(std::iter::repeat('-').take(rest_to_print)); + } + } + + write!(f, "{template}")?; + + Ok(()) +} + +fn write_record( + f: &mut fmt::Formatter<'_>, + field: &str, + value: &str, + max_field_width: usize, +) -> fmt::Result { + write!(f, "{field:max_field_width$} | {value}") +} + +fn truncate(text: &mut String, max: usize, suffix: &str) { + let original_len = text.len(); + + if max == 0 || text.is_empty() { + *text = String::new(); + } else { + *text = cut_str_basic(text, max).into_owned(); + } + + let cut_was_done = text.len() < original_len; + if !suffix.is_empty() && cut_was_done { + text.push_str(suffix); + } +} + +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) +} + +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(0); + + // 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) +} diff --git a/vendor/tabled/src/tables/iter.rs b/vendor/tabled/src/tables/iter.rs new file mode 100644 index 000000000..af485ac08 --- /dev/null +++ b/vendor/tabled/src/tables/iter.rs @@ -0,0 +1,344 @@ +//! This module contains a [`IterTable`] table. +//! +//! In contrast to [`Table`] [`IterTable`] does no allocations but it consumes an iterator. +//! It's useful when you don't want to re/allocate a buffer for your data. +//! +//! # Example +//! +//! ``` +//! use tabled::{grid::records::IterRecords, tables::IterTable}; +//! +//! let iterator = vec![vec!["First", "row"], vec!["Second", "row"]]; +//! let records = IterRecords::new(iterator, 2, Some(2)); +//! let table = IterTable::new(records); +//! +//! let s = table.to_string(); +//! +//! assert_eq!( +//! s, +//! "+--------+-----+\n\ +//! | First | row |\n\ +//! +--------+-----+\n\ +//! | Second | row |\n\ +//! +--------+-----+", +//! ); +//! ``` +//! +//! [`Table`]: crate::Table + +use std::{fmt, io}; + +use crate::{ + grid::{ + colors::NoColors, + config::{AlignmentHorizontal, CompactConfig, Indent, Sides, SpannedConfig}, + dimension::{CompactGridDimension, DimensionValue, StaticDimension}, + records::{ + into_records::{ + truncate_records::ExactValue, BufColumns, BufRows, LimitColumns, LimitRows, + TruncateContent, + }, + IntoRecords, IterRecords, + }, + Grid, + }, + settings::{Style, TableOption}, +}; + +use super::util::utf8_writer::UTF8Writer; + +/// A table which consumes an [`IntoRecords`] iterator. +/// +/// To be able to build table we need a dimensions. +/// If no width and count_columns is set, [`IterTable`] will sniff the records, by +/// keeping a number of rows buffered (You can set the number via [`IterTable::sniff`]). +#[derive(Debug, Clone)] +pub struct IterTable<I> { + records: I, + cfg: CompactConfig, + table: Settings, +} + +#[derive(Debug, Clone)] +struct Settings { + sniff: usize, + count_columns: Option<usize>, + count_rows: Option<usize>, + width: Option<usize>, + height: Option<usize>, +} + +impl<I> IterTable<I> { + /// Creates a new [`IterTable`] structure. + pub fn new(iter: I) -> Self + where + I: IntoRecords, + { + Self { + records: iter, + cfg: create_config(), + table: Settings { + sniff: 1000, + count_columns: None, + count_rows: None, + height: None, + width: None, + }, + } + } + + /// With is a generic function which applies options to the [`IterTable`]. + pub fn with<O>(mut self, option: O) -> Self + where + for<'a> O: TableOption<IterRecords<&'a I>, StaticDimension, CompactConfig>, + { + let count_columns = self.table.count_columns.unwrap_or(0); + let mut records = IterRecords::new(&self.records, count_columns, self.table.count_rows); + let mut dims = StaticDimension::new(DimensionValue::Exact(0), DimensionValue::Exact(1)); + option.change(&mut records, &mut self.cfg, &mut dims); + + self + } + + /// Limit a number of columns. + pub fn columns(mut self, count_columns: usize) -> Self { + self.table.count_columns = Some(count_columns); + self + } + + /// Limit a number of rows. + pub fn rows(mut self, count_rows: usize) -> Self { + self.table.count_rows = Some(count_rows); + self + } + + /// Limit an amount of rows will be read for dimension estimations. + pub fn sniff(mut self, count: usize) -> Self { + self.table.sniff = count; + self + } + + /// Set a height for each row. + pub fn height(mut self, size: usize) -> Self { + self.table.height = Some(size); + self + } + + /// Set a width for each column. + pub fn width(mut self, size: usize) -> Self { + self.table.width = Some(size); + self + } + + /// Build a string. + /// + /// We can't implement [`std::string::ToString`] cause it does takes `&self` reference. + #[allow(clippy::inherent_to_string)] + pub fn to_string(self) -> String + where + I: IntoRecords, + { + let mut buf = String::new(); + self.fmt(&mut buf).expect("safe"); + + buf + } + + /// Format table into [`io::Write`]r. + pub fn build<W>(self, writer: W) -> io::Result<()> + where + I: IntoRecords, + W: io::Write, + { + let writer = UTF8Writer::new(writer); + self.fmt(writer) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + } + + /// Format table into [fmt::Write]er. + pub fn fmt<W>(self, writer: W) -> fmt::Result + where + I: IntoRecords, + W: fmt::Write, + { + build_grid(writer, self.records, self.cfg, self.table) + } +} + +fn build_grid<W: fmt::Write, I: IntoRecords>( + f: W, + iter: I, + cfg: CompactConfig, + opts: Settings, +) -> fmt::Result { + let dont_sniff = opts.width.is_some() && opts.count_columns.is_some(); + if dont_sniff { + build_table_with_static_dims(f, iter, cfg, opts) + } else if opts.width.is_none() { + build_table_sniffing_with_unknown_width(f, iter, cfg, opts) + } else { + build_table_sniffing_with_known_width(f, iter, cfg, opts) + } +} + +fn build_table_with_static_dims<W, I>( + f: W, + iter: I, + cfg: CompactConfig, + opts: Settings, +) -> fmt::Result +where + W: fmt::Write, + I: IntoRecords, +{ + let count_columns = opts.count_columns.unwrap(); + let width = opts.width.unwrap(); + let height = opts.height.unwrap_or(1); + let contentw = ExactValue::Exact(width); + let pad = cfg.get_padding(); + let w = DimensionValue::Exact(width + pad.left.size + pad.right.size); + let h = DimensionValue::Exact(height + pad.top.size + pad.bottom.size); + let dims = StaticDimension::new(w, h); + let cfg = SpannedConfig::from(cfg); + + match opts.count_rows { + Some(limit) => { + let records = LimitRows::new(iter, limit); + let records = build_records(records, contentw, count_columns, Some(limit)); + Grid::new(records, dims, cfg, NoColors).build(f) + } + None => { + let records = build_records(iter, contentw, count_columns, None); + Grid::new(records, dims, cfg, NoColors).build(f) + } + } +} + +fn build_table_sniffing_with_unknown_width<W, I>( + f: W, + iter: I, + cfg: CompactConfig, + opts: Settings, +) -> fmt::Result +where + W: fmt::Write, + I: IntoRecords, +{ + let records = BufRows::new(iter, opts.sniff); + let records = BufColumns::from(records); + + let count_columns = get_count_columns(&opts, records.as_slice()); + + let (mut width, height) = { + let records = LimitColumns::new(records.as_slice(), count_columns); + let records = IterRecords::new(records, count_columns, None); + CompactGridDimension::dimension(records, &cfg) + }; + + let padding = cfg.get_padding(); + let pad = padding.left.size + padding.right.size; + let padv = padding.top.size + padding.bottom.size; + + if opts.sniff == 0 { + width = std::iter::repeat(pad) + .take(count_columns) + .collect::<Vec<_>>(); + } + + let content_width = ExactValue::List(width.iter().map(|i| i.saturating_sub(pad)).collect()); + let dims_width = DimensionValue::List(width); + + let height_exact = opts.height.unwrap_or(1) + padv; + let mut dims_height = DimensionValue::Partial(height, height_exact); + + if opts.height.is_some() { + dims_height = DimensionValue::Exact(height_exact); + } + + let dims = StaticDimension::new(dims_width, dims_height); + let cfg = SpannedConfig::from(cfg); + + match opts.count_rows { + Some(limit) => { + let records = LimitRows::new(records, limit); + let records = build_records(records, content_width, count_columns, Some(limit)); + Grid::new(records, dims, cfg, NoColors).build(f) + } + None => { + let records = build_records(records, content_width, count_columns, None); + Grid::new(records, dims, cfg, NoColors).build(f) + } + } +} + +fn build_table_sniffing_with_known_width<W, I>( + f: W, + iter: I, + cfg: CompactConfig, + opts: Settings, +) -> fmt::Result +where + W: fmt::Write, + I: IntoRecords, +{ + let records = BufRows::new(iter, opts.sniff); + let records = BufColumns::from(records); + + let count_columns = get_count_columns(&opts, records.as_slice()); + + let width = opts.width.unwrap(); + let contentw = ExactValue::Exact(width); + + let padding = cfg.get_padding(); + let pad = padding.left.size + padding.right.size; + let padv = padding.top.size + padding.bottom.size; + + let height = opts.height.unwrap_or(1) + padv; + let dimsh = DimensionValue::Exact(height); + let dimsw = DimensionValue::Exact(width + pad); + let dims = StaticDimension::new(dimsw, dimsh); + + let cfg = SpannedConfig::from(cfg); + + match opts.count_rows { + Some(limit) => { + let records = LimitRows::new(records, limit); + let records = build_records(records, contentw, count_columns, Some(limit)); + Grid::new(records, dims, cfg, NoColors).build(f) + } + None => { + let records = build_records(records, contentw, count_columns, None); + Grid::new(records, dims, cfg, NoColors).build(f) + } + } +} + +fn get_count_columns(opts: &Settings, buf: &[Vec<String>]) -> usize { + match opts.count_columns { + Some(size) => size, + None => buf.iter().map(|row| row.len()).max().unwrap_or(0), + } +} + +fn create_config() -> CompactConfig { + CompactConfig::default() + .set_padding(Sides::new( + Indent::spaced(1), + Indent::spaced(1), + Indent::default(), + Indent::default(), + )) + .set_alignment_horizontal(AlignmentHorizontal::Left) + .set_borders(*Style::ascii().get_borders()) +} + +fn build_records<I: IntoRecords>( + records: I, + width: ExactValue<'_>, + count_columns: usize, + count_rows: Option<usize>, +) -> IterRecords<LimitColumns<TruncateContent<'_, I>>> { + let records = TruncateContent::new(records, width); + let records = LimitColumns::new(records, count_columns); + IterRecords::new(records, count_columns, count_rows) +} diff --git a/vendor/tabled/src/tables/mod.rs b/vendor/tabled/src/tables/mod.rs new file mode 100644 index 000000000..e0c4bf794 --- /dev/null +++ b/vendor/tabled/src/tables/mod.rs @@ -0,0 +1,48 @@ +//! Module contains a list of table representatives. +//! +//! ## [`Table`] +//! +//! A default table implementation. +//! +//! ## [`IterTable`] +//! +//! Just like [`Table`] but it's API is a bit different to serve better in context +//! where there is a memory limit. +//! +//! ## [`ExtendedTable`] +//! +//! It's a table which is useful for large amount of data. +//! +//! ## [`PoolTable`] +//! +//! A table with a greather controll of a layout. + +mod compact; +mod util; + +#[cfg(feature = "std")] +mod extended; +#[cfg(feature = "std")] +mod iter; +#[cfg(feature = "std")] +mod table; +#[cfg(feature = "std")] +mod table_pool; + +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +pub use table::Table; + +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +pub use iter::IterTable; + +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +pub use extended::ExtendedTable; + +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +pub use table_pool::{PoolTable, TableValue}; + +pub use compact::CompactTable; diff --git a/vendor/tabled/src/tables/table.rs b/vendor/tabled/src/tables/table.rs new file mode 100644 index 000000000..d6ff37661 --- /dev/null +++ b/vendor/tabled/src/tables/table.rs @@ -0,0 +1,464 @@ +//! This module contains a main table representation [`Table`]. + +use core::ops::DerefMut; +use std::{borrow::Cow, fmt, iter::FromIterator}; + +use crate::{ + builder::Builder, + grid::{ + colors::NoColors, + config::{ + AlignmentHorizontal, ColorMap, ColoredConfig, CompactConfig, Entity, Formatting, + Indent, Sides, SpannedConfig, + }, + dimension::{CompleteDimensionVecRecords, Dimension, Estimate, PeekableDimension}, + records::{ + vec_records::{CellInfo, VecRecords}, + ExactRecords, Records, + }, + PeekableGrid, + }, + settings::{Style, TableOption}, + Tabled, +}; + +/// The structure provides an interface for building a table for types that implements [`Tabled`]. +/// +/// To build a string representation of a table you must use a [`std::fmt::Display`]. +/// Or simply call `.to_string()` method. +/// +/// The default table [`Style`] is [`Style::ascii`], +/// with a 1 left and right [`Padding`]. +/// +/// ## Example +/// +/// ### Basic usage +/// +/// ```rust,no_run +/// use tabled::Table; +/// +/// let table = Table::new(&["Year", "2021"]); +/// ``` +/// +/// ### With settings +/// +/// ```rust,no_run +/// use tabled::{Table, settings::{Style, Alignment}}; +/// +/// let data = vec!["Hello", "2021"]; +/// let mut table = Table::new(&data); +/// table.with(Style::psql()).with(Alignment::left()); +/// +/// println!("{}", table); +/// ``` +/// +/// [`Padding`]: crate::settings::Padding +/// [`Style`]: crate::settings::Style +/// [`Style::ascii`]: crate::settings::Style::ascii +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Table { + records: VecRecords<CellInfo<String>>, + config: ColoredConfig, + dimension: CompleteDimensionVecRecords<'static>, +} + +impl Table { + /// New creates a Table instance. + /// + /// If you use a reference iterator you'd better use [`FromIterator`] instead. + /// As it has a different lifetime constraints and make less copies therefore. + pub fn new<I, T>(iter: I) -> Self + where + I: IntoIterator<Item = T>, + T: Tabled, + { + let mut header = Vec::with_capacity(T::LENGTH); + for text in T::headers() { + let text = text.into_owned(); + let cell = CellInfo::new(text); + header.push(cell); + } + + let mut records = vec![header]; + for row in iter.into_iter() { + let mut list = Vec::with_capacity(T::LENGTH); + for text in row.fields().into_iter() { + let text = text.into_owned(); + let cell = CellInfo::new(text); + + list.push(cell); + } + + records.push(list); + } + + let records = VecRecords::new(records); + + Self { + records, + config: ColoredConfig::new(configure_grid()), + dimension: CompleteDimensionVecRecords::default(), + } + } + + /// Creates a builder from a data set given. + /// + /// # Example + /// + /// + #[cfg_attr(feature = "derive", doc = "```")] + #[cfg_attr(not(feature = "derive"), doc = "```ignore")] + /// use tabled::{ + /// Table, Tabled, + /// settings::{object::Segment, Modify, Alignment} + /// }; + /// + /// #[derive(Tabled)] + /// struct User { + /// name: &'static str, + /// #[tabled(inline("device::"))] + /// device: Device, + /// } + /// + /// #[derive(Tabled)] + /// enum Device { + /// PC, + /// Mobile + /// } + /// + /// let data = vec![ + /// User { name: "Vlad", device: Device::Mobile }, + /// User { name: "Dimitry", device: Device::PC }, + /// User { name: "John", device: Device::PC }, + /// ]; + /// + /// let mut table = Table::builder(data) + /// .index() + /// .column(0) + /// .transpose() + /// .build() + /// .with(Modify::new(Segment::new(1.., 1..)).with(Alignment::center())) + /// .to_string(); + /// + /// assert_eq!( + /// table, + /// "+----------------+------+---------+------+\n\ + /// | name | Vlad | Dimitry | John |\n\ + /// +----------------+------+---------+------+\n\ + /// | device::PC | | + | + |\n\ + /// +----------------+------+---------+------+\n\ + /// | device::Mobile | + | | |\n\ + /// +----------------+------+---------+------+" + /// ) + /// ``` + pub fn builder<I, T>(iter: I) -> Builder + where + T: Tabled, + I: IntoIterator<Item = T>, + { + let mut records = Vec::new(); + for row in iter { + let mut list = Vec::with_capacity(T::LENGTH); + for text in row.fields().into_iter() { + list.push(text.into_owned()); + } + + records.push(list); + } + + let mut b = Builder::from(records); + let _ = b.set_header(T::headers()).hint_column_size(T::LENGTH); + + b + } + + /// With is a generic function which applies options to the [`Table`]. + /// + /// It applies settings immediately. + pub fn with<O>(&mut self, option: O) -> &mut Self + where + O: TableOption< + VecRecords<CellInfo<String>>, + CompleteDimensionVecRecords<'static>, + ColoredConfig, + >, + { + self.dimension.clear_width(); + self.dimension.clear_height(); + + option.change(&mut self.records, &mut self.config, &mut self.dimension); + + self + } + + /// Returns a table shape (count rows, count columns). + pub fn shape(&self) -> (usize, usize) { + (self.count_rows(), self.count_columns()) + } + + /// Returns an amount of rows in the table. + pub fn count_rows(&self) -> usize { + self.records.count_rows() + } + + /// Returns an amount of columns in the table. + pub fn count_columns(&self) -> usize { + self.records.count_columns() + } + + /// Returns a table shape (count rows, count columns). + pub fn is_empty(&self) -> bool { + let (count_rows, count_cols) = self.shape(); + count_rows == 0 || count_cols == 0 + } + + /// Returns total widths of a table, including margin and horizontal lines. + pub fn total_height(&self) -> usize { + let mut dims = CompleteDimensionVecRecords::from_origin(&self.dimension); + dims.estimate(&self.records, self.config.as_ref()); + + let total = (0..self.count_rows()) + .map(|row| dims.get_height(row)) + .sum::<usize>(); + let counth = self.config.count_horizontal(self.count_rows()); + + let margin = self.config.get_margin(); + + total + counth + margin.top.size + margin.bottom.size + } + + /// Returns total widths of a table, including margin and vertical lines. + pub fn total_width(&self) -> usize { + let mut dims = CompleteDimensionVecRecords::from_origin(&self.dimension); + dims.estimate(&self.records, self.config.as_ref()); + + let total = (0..self.count_columns()) + .map(|col| dims.get_width(col)) + .sum::<usize>(); + let countv = self.config.count_vertical(self.count_columns()); + + let margin = self.config.get_margin(); + + total + countv + margin.left.size + margin.right.size + } + + /// Returns a table config. + pub fn get_config(&self) -> &ColoredConfig { + &self.config + } + + /// Returns a table config. + pub fn get_config_mut(&mut self) -> &mut ColoredConfig { + &mut self.config + } + + /// Returns a used records. + pub fn get_records(&self) -> &VecRecords<CellInfo<String>> { + &self.records + } + + /// Returns a used records. + pub fn get_records_mut(&mut self) -> &mut VecRecords<CellInfo<String>> { + &mut self.records + } +} + +impl Default for Table { + fn default() -> Self { + Self { + records: VecRecords::default(), + config: ColoredConfig::new(configure_grid()), + dimension: CompleteDimensionVecRecords::default(), + } + } +} + +impl fmt::Display for Table { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_empty() { + return Ok(()); + } + + let config = use_format_configuration(f, self); + let colors = self.config.get_colors(); + + if !self.dimension.is_empty() { + let mut dims = self.dimension.clone(); + dims.estimate(&self.records, config.as_ref()); + + print_grid(f, &self.records, &config, &dims, colors) + } else { + let mut dims = PeekableDimension::default(); + dims.estimate(&self.records, &config); + + print_grid(f, &self.records, &config, &dims, colors) + } + } +} + +impl<T, V> FromIterator<T> for Table +where + T: IntoIterator<Item = V>, + V: Into<String>, +{ + fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self { + Builder::from_iter(iter.into_iter().map(|i| i.into_iter().map(|s| s.into()))).build() + } +} + +impl From<Builder> for Table { + fn from(builder: Builder) -> Self { + let data: Vec<Vec<CellInfo<String>>> = builder.into(); + let records = VecRecords::new(data); + + Self { + records, + config: ColoredConfig::new(configure_grid()), + dimension: CompleteDimensionVecRecords::default(), + } + } +} + +impl From<Table> for Builder { + fn from(val: Table) -> Self { + let count_columns = val.count_columns(); + let data: Vec<Vec<CellInfo<String>>> = val.records.into(); + let mut builder = Builder::from(data); + let _ = builder.hint_column_size(count_columns); + builder + } +} + +impl<R, D> TableOption<R, D, ColoredConfig> for CompactConfig { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + *cfg.deref_mut() = self.into(); + } +} + +impl<R, D> TableOption<R, D, ColoredConfig> for ColoredConfig { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + *cfg = self; + } +} + +impl<R, D> TableOption<R, D, ColoredConfig> for SpannedConfig { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + *cfg.deref_mut() = self; + } +} + +impl<R, D> TableOption<R, D, ColoredConfig> for &SpannedConfig { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + *cfg.deref_mut() = self.clone(); + } +} + +fn convert_fmt_alignment(alignment: fmt::Alignment) -> AlignmentHorizontal { + match alignment { + fmt::Alignment::Left => AlignmentHorizontal::Left, + fmt::Alignment::Right => AlignmentHorizontal::Right, + fmt::Alignment::Center => AlignmentHorizontal::Center, + } +} + +fn table_padding(alignment: fmt::Alignment, available: usize) -> (usize, usize) { + match alignment { + fmt::Alignment::Left => (available, 0), + fmt::Alignment::Right => (0, available), + fmt::Alignment::Center => { + let left = available / 2; + let right = available - left; + (left, right) + } + } +} + +fn configure_grid() -> SpannedConfig { + let mut cfg = SpannedConfig::default(); + cfg.set_padding( + Entity::Global, + Sides::new( + Indent::spaced(1), + Indent::spaced(1), + Indent::default(), + Indent::default(), + ), + ); + cfg.set_alignment_horizontal(Entity::Global, AlignmentHorizontal::Left); + cfg.set_formatting(Entity::Global, Formatting::new(false, false, false)); + cfg.set_borders(*Style::ascii().get_borders()); + + cfg +} + +fn use_format_configuration<'a>( + f: &mut fmt::Formatter<'_>, + table: &'a Table, +) -> Cow<'a, SpannedConfig> { + if f.align().is_some() || f.width().is_some() { + let mut cfg = table.config.as_ref().clone(); + + set_align_table(f, &mut cfg); + set_width_table(f, &mut cfg, table); + + Cow::Owned(cfg) + } else { + Cow::Borrowed(table.config.as_ref()) + } +} + +fn set_align_table(f: &fmt::Formatter<'_>, cfg: &mut SpannedConfig) { + if let Some(alignment) = f.align() { + let alignment = convert_fmt_alignment(alignment); + cfg.set_alignment_horizontal(Entity::Global, alignment); + } +} + +fn set_width_table(f: &fmt::Formatter<'_>, cfg: &mut SpannedConfig, table: &Table) { + if let Some(width) = f.width() { + let total_width = table.total_width(); + if total_width >= width { + return; + } + + let mut fill = f.fill(); + if fill == char::default() { + fill = ' '; + } + + let available = width - total_width; + let alignment = f.align().unwrap_or(fmt::Alignment::Left); + let (left, right) = table_padding(alignment, available); + + let mut margin = cfg.get_margin(); + margin.left.size += left; + margin.right.size += right; + + if (margin.left.size > 0 && margin.left.fill == char::default()) || fill != char::default() + { + margin.left.fill = fill; + } + + if (margin.right.size > 0 && margin.right.fill == char::default()) + || fill != char::default() + { + margin.right.fill = fill; + } + + cfg.set_margin(margin); + } +} + +fn print_grid<F: fmt::Write, D: Dimension>( + f: &mut F, + records: &VecRecords<CellInfo<String>>, + cfg: &SpannedConfig, + dims: D, + colors: &ColorMap, +) -> fmt::Result { + if !colors.is_empty() { + PeekableGrid::new(records, cfg, &dims, colors).build(f) + } else { + PeekableGrid::new(records, cfg, &dims, NoColors).build(f) + } +} diff --git a/vendor/tabled/src/tables/table_pool.rs b/vendor/tabled/src/tables/table_pool.rs new file mode 100644 index 000000000..07d2a5437 --- /dev/null +++ b/vendor/tabled/src/tables/table_pool.rs @@ -0,0 +1,1607 @@ +use core::fmt::{self, Display, Formatter}; + +use crate::{ + grid::{ + config::{AlignmentHorizontal, CompactMultilineConfig, Indent, Sides}, + dimension::{DimensionPriority, PoolTableDimension}, + records::EmptyRecords, + records::IntoRecords, + }, + settings::{Style, TableOption}, +}; + +/// [`PoolTable`] is a table which allows a greater set of possibilities for cell alignment. +/// It's data is not aligned in any way by default. +/// +/// It works similar to the main [`Table`] by default. +/// +/// +/// ``` +/// use tabled::tables::PoolTable; +/// +/// let data = vec![ +/// vec!["Hello", "World", "!"], +/// vec!["Salve", "mondo", "!"], +/// vec!["Hola", "mundo", "!"], +/// ]; +/// +/// let table = PoolTable::new(data).to_string(); +/// +/// assert_eq!( +/// table, +/// "+-------+-------+---+\n\ +/// | Hello | World | ! |\n\ +/// +-------+-------+---+\n\ +/// | Salve | mondo | ! |\n\ +/// +-------+-------+---+\n\ +/// | Hola | mundo | ! |\n\ +/// +-------+-------+---+" +/// ) +/// ``` +/// +/// But it allows you to have a different number of columns inside the rows. +/// +/// ``` +/// use tabled::tables::PoolTable; +/// +/// let data = vec![ +/// vec!["Hello", "World", "!"], +/// vec!["Salve, mondo!"], +/// vec!["Hola", "mundo", "", "", "!"], +/// ]; +/// +/// let table = PoolTable::new(data).to_string(); +/// +/// assert_eq!( +/// table, +/// "+---------+---------+----+\n\ +/// | Hello | World | ! |\n\ +/// +---------+---------+----+\n\ +/// | Salve, mondo! |\n\ +/// +------+-------+--+--+---+\n\ +/// | Hola | mundo | | | ! |\n\ +/// +------+-------+--+--+---+" +/// ) +/// ``` +/// +/// Notice that you also can build a custom table layout by using [`TableValue`]. +/// +/// ``` +/// use tabled::tables::{PoolTable, TableValue}; +/// +/// let message = "Hello\nWorld"; +/// +/// let data = TableValue::Column(vec![ +/// TableValue::Row(vec![ +/// TableValue::Column(vec![ +/// TableValue::Cell(String::from(message)), +/// ]), +/// TableValue::Column(vec![ +/// TableValue::Cell(String::from(message)), +/// TableValue::Row(vec![ +/// TableValue::Cell(String::from(message)), +/// TableValue::Cell(String::from(message)), +/// TableValue::Cell(String::from(message)), +/// ]) +/// ]), +/// ]), +/// TableValue::Cell(String::from(message)), +/// ]); +/// +/// let table = PoolTable::from(data).to_string(); +/// +/// assert_eq!( +/// table, +/// "+-------+-----------------------+\n\ +/// | Hello | Hello |\n\ +/// | World | World |\n\ +/// | +-------+-------+-------+\n\ +/// | | Hello | Hello | Hello |\n\ +/// | | World | World | World |\n\ +/// +-------+-------+-------+-------+\n\ +/// | Hello |\n\ +/// | World |\n\ +/// +-------------------------------+" +/// ) +/// ``` +/// +/// [`Table`]: crate::Table +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct PoolTable { + config: CompactMultilineConfig, + dims: PoolTableDimension, + value: TableValue, +} + +impl PoolTable { + /// Creates a [`PoolTable`] out from a record iterator. + pub fn new<I: IntoRecords>(iter: I) -> Self { + let value = TableValue::Column( + iter.iter_rows() + .into_iter() + .map(|row| { + TableValue::Row( + row.into_iter() + .map(|cell| cell.as_ref().to_string()) + .map(TableValue::Cell) + .collect(), + ) + }) + .collect(), + ); + + Self { + config: configure_grid(), + dims: PoolTableDimension::new(DimensionPriority::List, DimensionPriority::List), + value, + } + } + + /// A is a generic function which applies options to the [`PoolTable`] configuration. + /// + /// Notice that it has a limited support of options. + /// + /// ``` + /// use tabled::tables::PoolTable; + /// use tabled::settings::{Style, Padding}; + /// + /// let data = vec![ + /// vec!["Hello", "World", "!"], + /// vec!["Salve", "mondo", "!"], + /// vec!["Hola", "mundo", "!"], + /// ]; + /// + /// let table = PoolTable::new(data) + /// .with(Style::extended()) + /// .with(Padding::zero()) + /// .to_string(); + /// + /// assert_eq!( + /// table, + /// "╔═════╦═════╦═╗\n\ + /// ║Hello║World║!║\n\ + /// ╠═════╬═════╬═╣\n\ + /// ║Salve║mondo║!║\n\ + /// ╠═════╬═════╬═╣\n\ + /// ║Hola ║mundo║!║\n\ + /// ╚═════╩═════╩═╝" + /// ) + /// ``` + pub fn with<O>(&mut self, option: O) -> &mut Self + where + O: TableOption<EmptyRecords, PoolTableDimension, CompactMultilineConfig>, + { + let mut records = EmptyRecords::default(); + option.change(&mut records, &mut self.config, &mut self.dims); + + self + } +} + +impl From<TableValue> for PoolTable { + fn from(value: TableValue) -> Self { + Self { + config: configure_grid(), + dims: PoolTableDimension::new(DimensionPriority::List, DimensionPriority::List), + value, + } + } +} + +impl Display for PoolTable { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + print::build_table(&self.value, &self.config, self.dims).fmt(f) + } +} + +/// [`TableValue`] a structure which is responsible for a [`PoolTable`] layout. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum TableValue { + /// A horizontal row. + Row(Vec<TableValue>), + /// A vertical column. + Column(Vec<TableValue>), + /// A single cell. + Cell(String), +} + +fn configure_grid() -> CompactMultilineConfig { + let pad = Sides::new( + Indent::spaced(1), + Indent::spaced(1), + Indent::default(), + Indent::default(), + ); + + CompactMultilineConfig::default() + .set_padding(pad) + .set_alignment_horizontal(AlignmentHorizontal::Left) + .set_borders(*Style::ascii().get_borders()) +} + +impl<R, C> TableOption<R, PoolTableDimension, C> for PoolTableDimension { + fn change(self, _: &mut R, _: &mut C, dimension: &mut PoolTableDimension) { + *dimension = self; + } +} + +impl<R, D> TableOption<R, D, CompactMultilineConfig> for CompactMultilineConfig { + fn change(self, _: &mut R, config: &mut CompactMultilineConfig, _: &mut D) { + *config = self; + } +} + +mod print { + use std::{cmp::max, collections::HashMap, iter::repeat}; + + use papergrid::{ + color::StaticColor, + config::{Border, Borders}, + util::string::string_width_multiline, + }; + + use crate::{ + builder::Builder, + grid::{ + config::{ + AlignmentHorizontal, AlignmentVertical, ColoredConfig, CompactMultilineConfig, + Indent, Offset, Sides, + }, + dimension::{Dimension, DimensionPriority, Estimate, PoolTableDimension}, + records::Records, + util::string::{count_lines, get_lines, string_dimension, string_width}, + }, + settings::{Padding, Style, TableOption}, + }; + + use super::TableValue; + + #[derive(Debug, Default)] + struct PrintContext { + pos: usize, + is_last_col: bool, + is_last_row: bool, + is_first_col: bool, + is_first_row: bool, + kv: bool, + kv_is_first: bool, + list: bool, + list_is_first: bool, + no_left: bool, + no_right: bool, + no_bottom: bool, + lean_top: bool, + top_intersection: bool, + top_left: bool, + intersections_horizontal: Vec<usize>, + intersections_vertical: Vec<usize>, + size: Dim, + } + + struct CellData { + content: String, + intersections_horizontal: Vec<usize>, + intersections_vertical: Vec<usize>, + } + + impl CellData { + fn new(content: String, i_horizontal: Vec<usize>, i_vertical: Vec<usize>) -> Self { + Self { + content, + intersections_horizontal: i_horizontal, + intersections_vertical: i_vertical, + } + } + } + + pub(super) fn build_table( + val: &TableValue, + cfg: &CompactMultilineConfig, + dims_priority: PoolTableDimension, + ) -> String { + let dims = collect_table_dimensions(val, cfg); + let ctx = PrintContext { + is_last_col: true, + is_last_row: true, + is_first_col: true, + is_first_row: true, + size: *dims.all.get(&0).unwrap(), + ..Default::default() + }; + + let data = _build_table(val, cfg, &dims, dims_priority, ctx); + let mut table = data.content; + + let margin = cfg.get_margin(); + let has_margin = margin.top.size > 0 + || margin.bottom.size > 0 + || margin.left.size > 0 + || margin.right.size > 0; + if has_margin { + let color = convert_border_colors(cfg.get_margin_color()); + table = set_margin(&table, *margin, color); + } + + table + } + + fn _build_table( + val: &TableValue, + cfg: &CompactMultilineConfig, + dims: &Dimensions, + priority: PoolTableDimension, + ctx: PrintContext, + ) -> CellData { + match val { + TableValue::Cell(text) => generate_value_cell(text, cfg, ctx), + TableValue::Row(list) => { + if list.is_empty() { + return generate_value_cell("", cfg, ctx); + } + + generate_table_row(list, cfg, dims, priority, ctx) + } + TableValue::Column(list) => { + if list.is_empty() { + return generate_value_cell("", cfg, ctx); + } + + generate_table_column(list, cfg, dims, priority, ctx) + } + } + } + + fn generate_table_column( + list: &Vec<TableValue>, + cfg: &CompactMultilineConfig, + dims: &Dimensions, + priority: PoolTableDimension, + ctx: PrintContext, + ) -> CellData { + let array_dims = dims.arrays.get(&ctx.pos).unwrap(); + + let height = dims.all.get(&ctx.pos).unwrap().height; + let additional_height = ctx.size.height - height; + let (chunk_height, mut rest_height) = split_value(additional_height, list.len()); + + let mut intersections_horizontal = ctx.intersections_horizontal; + let mut intersections_vertical = ctx.intersections_vertical; + let mut next_vsplit = false; + let mut next_intersections_vertical = vec![]; + + let mut builder = Builder::new(); + for (i, val) in list.iter().enumerate() { + let val_pos = *array_dims.index.get(&i).unwrap(); + + let mut height = dims.all.get(&val_pos).unwrap().height; + match priority.height() { + DimensionPriority::First => { + if i == 0 { + height += additional_height; + } + } + DimensionPriority::Last => { + if i + 1 == list.len() { + height += additional_height; + } + } + DimensionPriority::List => { + height += chunk_height; + + if rest_height > 0 { + height += 1; + rest_height -= 1; // must be safe + } + } + } + + let size = Dim::new(ctx.size.width, height); + + let (split, intersections_vertical) = + short_splits3(&mut intersections_vertical, size.height); + let old_split = next_vsplit; + next_vsplit = split; + + let is_prev_list_not_first = ctx.list && !ctx.list_is_first; + let valctx = PrintContext { + pos: val_pos, + is_last_col: ctx.is_last_col, + is_last_row: ctx.is_last_row && i + 1 == list.len(), + is_first_col: ctx.is_first_col, + is_first_row: ctx.is_first_row && i == 0, + kv: ctx.kv, + kv_is_first: ctx.kv_is_first, + list: true, + list_is_first: i == 0 && !is_prev_list_not_first, + no_left: ctx.no_left, + no_right: ctx.no_right, + no_bottom: ctx.no_bottom && i + 1 == list.len(), + lean_top: ctx.lean_top && i == 0, + top_intersection: (ctx.top_intersection && i == 0) || old_split, + top_left: ctx.top_left || i > 0, + intersections_horizontal, + intersections_vertical, + size, + }; + + let data = _build_table(val, cfg, dims, priority, valctx); + intersections_horizontal = data.intersections_horizontal; + next_intersections_vertical.extend(data.intersections_vertical); + + let _ = builder.push_record([data.content]); + } + + let table = builder + .build() + .with(Style::empty()) + .with(Padding::zero()) + .to_string(); + + CellData::new(table, intersections_horizontal, next_intersections_vertical) + } + + fn generate_table_row( + list: &Vec<TableValue>, + cfg: &CompactMultilineConfig, + dims: &Dimensions, + priority: PoolTableDimension, + ctx: PrintContext, + ) -> CellData { + let array_dims = dims.arrays.get(&ctx.pos).unwrap(); + + let list_width = dims.all.get(&ctx.pos).unwrap().width; + let additional_width = ctx.size.width - list_width; + let (chunk_width, mut rest_width) = split_value(additional_width, list.len()); + + let mut intersections_horizontal = ctx.intersections_horizontal; + let mut intersections_vertical = ctx.intersections_vertical; + let mut new_intersections_horizontal = vec![]; + let mut split_next = false; + + let mut buf = Vec::with_capacity(list.len()); + for (i, val) in list.iter().enumerate() { + let val_pos = *array_dims.index.get(&i).unwrap(); + + let mut width = dims.all.get(&val_pos).unwrap().width; + match priority.width() { + DimensionPriority::First => { + if i == 0 { + width += additional_width; + } + } + DimensionPriority::Last => { + if i + 1 == list.len() { + width += additional_width; + } + } + DimensionPriority::List => { + width += chunk_width; + + if rest_width > 0 { + width += 1; + rest_width -= 1; // must be safe + } + } + } + + let size = Dim::new(width, ctx.size.height); + + let (split, intersections_horizontal) = + short_splits3(&mut intersections_horizontal, width); + let old_split = split_next; + split_next = split; + + let is_prev_list_not_first = ctx.list && !ctx.list_is_first; + let valctx = PrintContext { + pos: val_pos, + is_first_col: ctx.is_first_col && i == 0, + is_last_col: ctx.is_last_col && i + 1 == list.len(), + is_last_row: ctx.is_last_row, + is_first_row: ctx.is_first_row, + kv: false, + kv_is_first: false, + list: false, + list_is_first: !is_prev_list_not_first, + no_left: false, + no_right: !(ctx.is_last_col && i + 1 == list.len()), + no_bottom: false, + lean_top: !(ctx.is_first_col && i == 0), + top_intersection: (ctx.top_intersection && i == 0) || old_split, + top_left: ctx.top_left && i == 0, + intersections_horizontal, + intersections_vertical, + size, + }; + + let val = _build_table(val, cfg, dims, priority, valctx); + intersections_vertical = val.intersections_vertical; + new_intersections_horizontal.extend(val.intersections_horizontal.iter()); + let value = val.content; + + buf.push(value); + } + + let mut b = Builder::with_capacity(1); + let _ = b.hint_column_size(buf.len()).push_record(buf); + let table = b + .build() + .with(Style::empty()) + .with(Padding::zero()) + .to_string(); + + CellData::new(table, new_intersections_horizontal, intersections_vertical) + } + + fn generate_value_cell( + text: &str, + cfg: &CompactMultilineConfig, + ctx: PrintContext, + ) -> CellData { + let width = ctx.size.width; + let height = ctx.size.height; + let table = generate_value_table(text, cfg, ctx); + CellData::new(table, vec![width], vec![height]) + } + + fn generate_value_table( + text: &str, + cfg: &CompactMultilineConfig, + mut ctx: PrintContext, + ) -> String { + if ctx.size.width == 0 || ctx.size.height == 0 { + return String::new(); + } + + let halignment = cfg.get_alignment_horizontal(); + let valignment = cfg.get_alignment_vertical(); + let pad = cfg.get_padding(); + let pad_color = cfg.get_padding_color(); + let pad_color = convert_border_colors(pad_color); + let lines_alignemnt = cfg.get_formatting().allow_lines_alignment; + + let mut borders = *cfg.get_borders(); + + let bottom_intesection = cfg.get_borders().bottom_intersection.unwrap_or(' '); + let mut horizontal_splits = short_splits(&mut ctx.intersections_horizontal, ctx.size.width); + squash_splits(&mut horizontal_splits); + + let right_intersection = borders.right_intersection.unwrap_or(' '); + let mut vertical_splits = short_splits(&mut ctx.intersections_vertical, ctx.size.height); + squash_splits(&mut vertical_splits); + + config_borders(&mut borders, &ctx); + let border = create_border(borders); + + let borders_colors = *cfg.get_borders_color(); + let border_color = create_border(borders_colors); + + let mut height = ctx.size.height; + height -= pad.top.size + pad.bottom.size; + + let mut width = ctx.size.width; + width -= pad.left.size + pad.right.size; + + let count_lines = count_lines(text); + let (top, bottom) = indent_vertical(valignment, height, count_lines); + + let mut buf = String::new(); + print_top_line( + &mut buf, + border, + border_color, + &horizontal_splits, + bottom_intesection, + ctx.size.width, + ); + + let mut line_index = 0; + let mut vertical_splits = &vertical_splits[..]; + + for _ in 0..top { + let mut border = border; + if vertical_splits.first() == Some(&line_index) { + border.left = Some(right_intersection); + vertical_splits = &vertical_splits[1..]; + } + + print_line(&mut buf, border, border_color, None, ' ', ctx.size.width); + line_index += 1; + } + + for _ in 0..pad.top.size { + let mut border = border; + if vertical_splits.first() == Some(&line_index) { + border.left = Some(right_intersection); + vertical_splits = &vertical_splits[1..]; + } + + print_line( + &mut buf, + border, + border_color, + pad_color.top, + pad.top.fill, + ctx.size.width, + ); + line_index += 1; + } + + if lines_alignemnt { + for line in get_lines(text) { + let line_width = string_width(&line); + let (left, right) = indent_horizontal(halignment, width, line_width); + + if border.has_left() { + let mut c = border.left.unwrap_or(' '); + if vertical_splits.first() == Some(&line_index) { + c = right_intersection; + vertical_splits = &vertical_splits[1..]; + } + + print_char(&mut buf, c, border_color.left); + } + + print_chars(&mut buf, pad.left.fill, pad_color.left, pad.left.size); + buf.extend(repeat(' ').take(left)); + buf.push_str(&line); + buf.extend(repeat(' ').take(right)); + print_chars(&mut buf, pad.right.fill, pad_color.right, pad.right.size); + + if border.has_right() { + print_char(&mut buf, border.right.unwrap_or(' '), border_color.right); + } + + buf.push('\n'); + + line_index += 1; + } + } else { + let text_width = string_width_multiline(text); + let (left, _) = indent_horizontal(halignment, width, text_width); + + for line in get_lines(text) { + let line_width = string_width(&line); + let right = width - line_width - left; + + if border.has_left() { + let mut c = border.left.unwrap_or(' '); + if vertical_splits.first() == Some(&line_index) { + c = right_intersection; + vertical_splits = &vertical_splits[1..]; + } + + print_char(&mut buf, c, border_color.left); + } + + print_chars(&mut buf, pad.left.fill, pad_color.left, pad.left.size); + buf.extend(repeat(' ').take(left)); + buf.push_str(&line); + buf.extend(repeat(' ').take(right)); + print_chars(&mut buf, pad.right.fill, pad_color.right, pad.right.size); + + if border.has_right() { + print_char(&mut buf, border.right.unwrap_or(' '), border_color.right); + } + + buf.push('\n'); + + line_index += 1; + } + } + + for _ in 0..pad.bottom.size { + let mut border = border; + if vertical_splits.first() == Some(&line_index) { + border.left = Some(right_intersection); + vertical_splits = &vertical_splits[1..]; + } + + print_line( + &mut buf, + border, + border_color, + pad_color.bottom, + pad.bottom.fill, + ctx.size.width, + ); + + line_index += 1; + } + + for _ in 0..bottom { + let mut border = border; + if vertical_splits.first() == Some(&line_index) { + border.left = Some(right_intersection); + vertical_splits = &vertical_splits[1..]; + } + + print_line(&mut buf, border, border_color, None, ' ', ctx.size.width); + line_index += 1; + } + + print_bottom_line(&mut buf, border, border_color, ctx.size.width); + + let _ = buf.remove(buf.len() - 1); + + buf + } + + fn print_chars(buf: &mut String, c: char, color: Option<StaticColor>, width: usize) { + match color { + Some(color) => { + buf.push_str(color.get_prefix()); + buf.extend(repeat(c).take(width)); + buf.push_str(color.get_suffix()); + } + None => buf.extend(repeat(c).take(width)), + } + } + + fn print_char(buf: &mut String, c: char, color: Option<StaticColor>) { + match color { + Some(color) => { + buf.push_str(color.get_prefix()); + buf.push(c); + buf.push_str(color.get_suffix()); + } + None => buf.push(c), + } + } + + fn print_line( + buf: &mut String, + border: Border<char>, + border_color: Border<StaticColor>, + color: Option<StaticColor>, + c: char, + width: usize, + ) { + if border.has_left() { + let c = border.left.unwrap_or(' '); + print_char(buf, c, border_color.left); + } + + print_chars(buf, c, color, width); + + if border.has_right() { + let c = border.right.unwrap_or(' '); + print_char(buf, c, border_color.right); + } + + buf.push('\n'); + } + + fn print_top_line( + buf: &mut String, + border: Border<char>, + color: Border<StaticColor>, + splits: &[usize], + split_char: char, + width: usize, + ) { + if !border.has_top() { + return; + } + + let mut used_color: Option<StaticColor> = None; + + if border.has_left() { + if let Some(color) = color.left_top_corner { + used_color = Some(color); + buf.push_str(color.get_prefix()); + } + + let c = border.left_top_corner.unwrap_or(' '); + buf.push(c); + } + + if let Some(color) = color.top { + match used_color { + Some(used) => { + if used != color { + buf.push_str(used.get_suffix()); + buf.push_str(color.get_prefix()); + } + } + None => { + buf.push_str(color.get_prefix()); + used_color = Some(color); + } + } + } + + let c = border.top.unwrap_or(' '); + if splits.is_empty() { + buf.extend(repeat(c).take(width)); + } else { + let mut splits = splits; + for i in 0..width { + if splits.first() == Some(&i) { + buf.push(split_char); + splits = &splits[1..]; + } else { + buf.push(c); + } + } + } + + if border.has_right() { + if let Some(color) = color.right_top_corner { + match used_color { + Some(used) => { + if used != color { + buf.push_str(used.get_suffix()); + buf.push_str(color.get_prefix()); + } + } + None => { + buf.push_str(color.get_prefix()); + used_color = Some(color); + } + } + } + + let c = border.right_top_corner.unwrap_or(' '); + buf.push(c); + } + + if let Some(used) = used_color { + buf.push_str(used.get_suffix()); + } + + buf.push('\n'); + } + + fn print_bottom_line( + buf: &mut String, + border: Border<char>, + color: Border<StaticColor>, + width: usize, + ) { + if !border.has_bottom() { + return; + } + + let mut used_color: Option<StaticColor> = None; + + if border.has_left() { + if let Some(color) = color.left_bottom_corner { + used_color = Some(color); + buf.push_str(color.get_prefix()); + } + + let c = border.left_bottom_corner.unwrap_or(' '); + buf.push(c); + } + + if let Some(color) = color.bottom { + match used_color { + Some(used) => { + if used != color { + buf.push_str(used.get_suffix()); + buf.push_str(color.get_prefix()); + } + } + None => { + buf.push_str(color.get_prefix()); + used_color = Some(color); + } + } + } + + let c = border.bottom.unwrap_or(' '); + buf.extend(repeat(c).take(width)); + + if border.has_right() { + if let Some(color) = color.right_bottom_corner { + match used_color { + Some(used) => { + if used != color { + buf.push_str(used.get_suffix()); + buf.push_str(color.get_prefix()); + } + } + None => { + buf.push_str(color.get_prefix()); + used_color = Some(color); + } + } + } + + let c = border.right_bottom_corner.unwrap_or(' '); + buf.push(c); + } + + if let Some(used) = used_color { + buf.push_str(used.get_suffix()); + } + + buf.push('\n'); + } + + fn create_border<T>(borders: Borders<T>) -> Border<T> { + Border { + top: borders.top, + bottom: borders.bottom, + left: borders.left, + right: borders.right, + left_top_corner: borders.top_left, + left_bottom_corner: borders.bottom_left, + right_top_corner: borders.top_right, + right_bottom_corner: borders.bottom_right, + } + } + + fn config_borders(borders: &mut Borders<char>, ctx: &PrintContext) { + // set top_left + { + if ctx.kv && ctx.kv_is_first { + borders.top_left = borders.top_intersection; + } + + if ctx.kv && !ctx.kv_is_first { + borders.top_left = borders.intersection; + } + + if ctx.kv && ctx.list && !ctx.list_is_first { + borders.top_left = borders.left_intersection; + } + + if ctx.is_first_col && !ctx.is_first_row { + borders.top_left = borders.left_intersection; + } + + if ctx.lean_top { + borders.top_left = borders.top_intersection; + } + + if ctx.top_left { + borders.top_left = borders.left_intersection; + } + + if ctx.top_intersection { + borders.top_left = borders.intersection; + } + } + + if ctx.is_last_col && !ctx.is_first_row { + borders.top_right = borders.right_intersection; + } + + if !ctx.is_first_col && ctx.is_last_row { + borders.bottom_left = borders.bottom_intersection; + } + + if !ctx.is_last_row || ctx.no_bottom { + cfg_no_bottom_borders(borders); + } + + if ctx.no_right { + cfg_no_right_borders(borders); + } + } + + struct ConfigCell(PrintContext); + + impl<R, D> TableOption<R, D, ColoredConfig> for ConfigCell { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + { + // we set a horizontal lines to borders to not complicate logic with cleaning it + + let mut borders = *cfg.get_borders(); + if let Some(line) = cfg.get_horizontal_line(0) { + borders.top = line.main; + borders.top_left = line.left; + borders.top_right = line.right; + } + + if let Some(line) = cfg.get_horizontal_line(1) { + borders.bottom = line.main; + borders.bottom_left = line.left; + borders.bottom_right = line.right; + } + + cfg.clear_theme(); + cfg.set_borders(borders); + } + + let mut ctx = self.0; + + let has_vertical = cfg.get_borders().has_left(); + if !ctx.intersections_horizontal.is_empty() && has_vertical { + let mut splits = short_splits(&mut ctx.intersections_horizontal, ctx.size.width); + squash_splits(&mut splits); + + let c = cfg.get_borders().bottom_intersection.unwrap_or(' '); + cfg_set_top_chars(cfg, &splits, c) + } + + let has_horizontal = cfg.get_borders().has_top(); + if !ctx.intersections_vertical.is_empty() && has_horizontal { + let mut splits = short_splits(&mut ctx.intersections_vertical, ctx.size.height); + squash_splits(&mut splits); + + let c = cfg.get_borders().right_intersection.unwrap_or(' '); + cfg_set_left_chars(cfg, &splits, c) + } + + let mut borders = *cfg.get_borders(); + + // set top_left + { + if ctx.kv && ctx.kv_is_first { + borders.top_left = borders.top_intersection; + } + + if ctx.kv && !ctx.kv_is_first { + borders.top_left = borders.intersection; + } + + if ctx.kv && ctx.list && !ctx.list_is_first { + borders.top_left = borders.left_intersection; + } + + if ctx.is_first_col && !ctx.is_first_row { + borders.top_left = borders.left_intersection; + } + + if ctx.lean_top { + borders.top_left = borders.top_intersection; + } + + if ctx.top_left { + borders.top_left = borders.left_intersection; + } + + if ctx.top_intersection { + borders.top_left = borders.intersection; + } + } + + if ctx.is_last_col && !ctx.is_first_row { + borders.top_right = borders.right_intersection; + } + + if !ctx.is_first_col && ctx.is_last_row { + borders.bottom_left = borders.bottom_intersection; + } + + if !ctx.is_last_row || ctx.no_bottom { + cfg_no_bottom_borders(&mut borders); + } + + if ctx.no_right { + cfg_no_right_borders(&mut borders); + } + + cfg.set_borders(borders); + } + } + + fn cfg_no_bottom_borders(borders: &mut Borders<char>) { + borders.bottom = None; + borders.bottom_intersection = None; + borders.bottom_left = None; + borders.bottom_right = None; + borders.horizontal = None; + } + + fn cfg_no_right_borders(borders: &mut Borders<char>) { + borders.right = None; + borders.right_intersection = None; + borders.top_right = None; + borders.bottom_right = None; + borders.vertical = None; + } + + fn cfg_set_top_chars(cfg: &mut ColoredConfig, list: &[usize], c: char) { + for &split in list { + let offset = split; + cfg.set_horizontal_char((0, 0), c, Offset::Begin(offset)); + } + } + + fn cfg_set_left_chars(cfg: &mut ColoredConfig, list: &[usize], c: char) { + for &offset in list { + cfg.set_vertical_char((0, 0), c, Offset::Begin(offset)); + } + } + + struct NoTopBorders; + + impl<R, D> TableOption<R, D, ColoredConfig> for NoTopBorders { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.top = None; + borders.top_intersection = None; + borders.top_left = None; + borders.top_right = None; + + cfg.set_borders(borders); + } + } + + struct NoBottomBorders; + + impl<R, D> TableOption<R, D, ColoredConfig> for NoBottomBorders { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.bottom = None; + borders.bottom_intersection = None; + borders.bottom_left = None; + borders.bottom_right = None; + + cfg.set_borders(borders); + } + } + + struct NoRightBorders; + + impl<R, D> TableOption<R, D, ColoredConfig> for NoRightBorders { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.top_right = None; + borders.bottom_right = None; + borders.right = None; + borders.right_intersection = None; + + cfg.set_borders(borders); + } + } + + struct NoLeftBorders; + + impl<R, D> TableOption<R, D, ColoredConfig> for NoLeftBorders { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.top_left = None; + borders.bottom_left = None; + borders.left = None; + borders.left_intersection = None; + + cfg.set_borders(borders); + } + } + + struct TopLeftChangeTopIntersection; + + impl<R, D> TableOption<R, D, ColoredConfig> for TopLeftChangeTopIntersection { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.top_left = borders.top_intersection; + + cfg.set_borders(borders); + } + } + + struct TopLeftChangeIntersection; + + impl<R, D> TableOption<R, D, ColoredConfig> for TopLeftChangeIntersection { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.top_left = borders.intersection; + + cfg.set_borders(borders); + } + } + + struct TopLeftChangeToLeft; + + impl<R, D> TableOption<R, D, ColoredConfig> for TopLeftChangeToLeft { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.top_left = borders.left_intersection; + + cfg.set_borders(borders); + } + } + + struct TopRightChangeToRight; + + impl<R, D> TableOption<R, D, ColoredConfig> for TopRightChangeToRight { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.top_right = borders.right_intersection; + + cfg.set_borders(borders); + } + } + + struct BottomLeftChangeSplit; + + impl<R, D> TableOption<R, D, ColoredConfig> for BottomLeftChangeSplit { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.bottom_left = borders.left_intersection; + + cfg.set_borders(borders); + } + } + + struct BottomLeftChangeSplitToIntersection; + + impl<R, D> TableOption<R, D, ColoredConfig> for BottomLeftChangeSplitToIntersection { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.bottom_left = borders.intersection; + + cfg.set_borders(borders); + } + } + + struct BottomRightChangeToRight; + + impl<R, D> TableOption<R, D, ColoredConfig> for BottomRightChangeToRight { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.bottom_right = borders.right_intersection; + + cfg.set_borders(borders); + } + } + + struct BottomLeftChangeToBottomIntersection; + + impl<R, D> TableOption<R, D, ColoredConfig> for BottomLeftChangeToBottomIntersection { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + let mut borders = *cfg.get_borders(); + borders.bottom_left = borders.bottom_intersection; + + cfg.set_borders(borders); + } + } + + struct SetBottomChars<'a>(&'a [usize], char); + + impl<R, D> TableOption<R, D, ColoredConfig> for SetBottomChars<'_> + where + R: Records, + for<'a> &'a R: Records, + for<'a> D: Dimension + Estimate<&'a R, ColoredConfig>, + { + fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) { + dims.estimate(&*records, cfg); + + let table_width = (0..records.count_columns()) + .map(|col| dims.get_width(col)) + .sum::<usize>() + + cfg.count_vertical(records.count_columns()); + let mut current_width = 0; + + for pos in self.0 { + current_width += pos; + if current_width > table_width { + break; + } + + let split_char = self.1; + cfg.set_horizontal_char((1, 0), split_char, Offset::Begin(current_width)); + + current_width += 1; + } + } + } + + struct SetTopChars<'a>(&'a [usize], char); + + impl<R, D> TableOption<R, D, ColoredConfig> for SetTopChars<'_> { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + for &split in self.0 { + let offset = split; + cfg.set_horizontal_char((0, 0), self.1, Offset::Begin(offset)); + } + } + } + + struct SetLeftChars<'a>(&'a [usize], char); + + impl<R, D> TableOption<R, D, ColoredConfig> for SetLeftChars<'_> { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + for &offset in self.0 { + cfg.set_vertical_char((0, 0), self.1, Offset::Begin(offset)); + } + } + } + + struct GetTopIntersection(char); + + impl<R, D> TableOption<R, D, ColoredConfig> for &mut GetTopIntersection { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + self.0 = cfg.get_borders().top_intersection.unwrap_or(' '); + } + } + + struct GetBottomIntersection(char); + + impl<R, D> TableOption<R, D, ColoredConfig> for &mut GetBottomIntersection { + fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) { + self.0 = cfg.get_borders().bottom_intersection.unwrap_or(' '); + } + } + + #[derive(Debug, Default)] + struct Dimensions { + all: HashMap<usize, Dim>, + arrays: HashMap<usize, ArrayDimensions>, + } + + #[derive(Debug, Default, Clone, Copy)] + struct Dim { + width: usize, + height: usize, + } + + impl Dim { + fn new(width: usize, height: usize) -> Self { + Self { width, height } + } + } + + #[derive(Debug, Default)] + struct ArrayDimensions { + max: Dim, + index: HashMap<usize, usize>, + } + + fn collect_table_dimensions(val: &TableValue, cfg: &CompactMultilineConfig) -> Dimensions { + let mut buf = Dimensions::default(); + let (dim, _) = __collect_table_dims(&mut buf, val, cfg, 0); + let _ = buf.all.insert(0, dim); + buf + } + + fn __collect_table_dims( + buf: &mut Dimensions, + val: &TableValue, + cfg: &CompactMultilineConfig, + pos: usize, + ) -> (Dim, usize) { + match val { + TableValue::Cell(text) => (str_dimension(text, cfg), 0), + TableValue::Row(list) => { + if list.is_empty() { + return (empty_dimension(cfg), 0); + } + + let mut index = ArrayDimensions { + max: Dim::default(), + index: HashMap::with_capacity(list.len()), + }; + + let mut total_width = 0; + + let mut count_elements = list.len(); + let mut val_pos = pos + 1; + for (i, value) in list.iter().enumerate() { + let (dim, elements) = __collect_table_dims(buf, value, cfg, val_pos); + count_elements += elements; + + total_width += dim.width; + + index.max.width = max(index.max.width, dim.width); + index.max.height = max(index.max.height, dim.height); + + let _ = buf.all.insert(val_pos, dim); + + let _ = index.index.insert(i, val_pos); + + val_pos += 1 + elements; + } + + let max_height = index.max.height; + + let _ = buf.arrays.insert(pos, index); + + let has_vertical = cfg.get_borders().has_left(); + total_width += has_vertical as usize * (list.len() - 1); + + (Dim::new(total_width, max_height), count_elements) + } + TableValue::Column(list) => { + if list.is_empty() { + return (empty_dimension(cfg), 0); + } + + let mut index = ArrayDimensions { + max: Dim::default(), + index: HashMap::with_capacity(list.len()), + }; + + let mut total_height = 0; + + let mut count_elements = list.len(); + let mut val_pos = pos + 1; + for (i, value) in list.iter().enumerate() { + let (dim, elements) = __collect_table_dims(buf, value, cfg, val_pos); + count_elements += elements; + + total_height += dim.height; + + index.max.width = max(index.max.width, dim.width); + index.max.height = max(index.max.height, dim.height); + + let _ = buf.all.insert(val_pos, dim); + + let _ = index.index.insert(i, val_pos); + + val_pos += 1 + elements; + } + + let max_width = index.max.width; + + let _ = buf.arrays.insert(pos, index); + + let has_horizontal = cfg.get_borders().has_top(); + total_height += has_horizontal as usize * (list.len() - 1); + + (Dim::new(max_width, total_height), count_elements) + } + } + } + + fn empty_dimension(cfg: &CompactMultilineConfig) -> Dim { + Dim::new(get_padding_horizontal(cfg), 1 + get_padding_vertical(cfg)) + } + + fn str_dimension(text: &str, cfg: &CompactMultilineConfig) -> Dim { + let (count_lines, width) = string_dimension(text); + let w = width + get_padding_horizontal(cfg); + let h = count_lines + get_padding_vertical(cfg); + Dim::new(w, h) + } + + fn get_padding_horizontal(cfg: &CompactMultilineConfig) -> usize { + let pad = cfg.get_padding(); + pad.left.size + pad.right.size + } + + fn get_padding_vertical(cfg: &CompactMultilineConfig) -> usize { + let pad = cfg.get_padding(); + pad.top.size + pad.bottom.size + } + + fn split_value(value: usize, by: usize) -> (usize, usize) { + let val = value / by; + let rest = value - val * by; + (val, rest) + } + + fn indent_vertical(al: AlignmentVertical, available: usize, real: usize) -> (usize, usize) { + let top = indent_top(al, available, real); + let bottom = available - real - top; + (top, bottom) + } + + fn indent_horizontal(al: AlignmentHorizontal, available: usize, real: usize) -> (usize, usize) { + let top = indent_left(al, available, real); + let right = available - real - top; + (top, right) + } + + fn indent_top(al: AlignmentVertical, available: usize, real: usize) -> usize { + match al { + AlignmentVertical::Top => 0, + AlignmentVertical::Bottom => available - real, + AlignmentVertical::Center => (available - real) / 2, + } + } + + fn indent_left(al: AlignmentHorizontal, available: usize, real: usize) -> usize { + match al { + AlignmentHorizontal::Left => 0, + AlignmentHorizontal::Right => available - real, + AlignmentHorizontal::Center => (available - real) / 2, + } + } + + fn short_splits(splits: &mut Vec<usize>, width: usize) -> Vec<usize> { + if splits.is_empty() { + return Vec::new(); + } + + let mut out = Vec::new(); + let mut pos = 0; + for &split in splits.iter() { + if pos + split >= width { + break; + } + + pos += split; + out.push(pos); + } + + let _ = splits.drain(..out.len()); + + if !splits.is_empty() && pos <= width { + let rest = width - pos; + splits[0] -= rest; + } + + out + } + + fn short_splits3(splits: &mut Vec<usize>, width: usize) -> (bool, Vec<usize>) { + if splits.is_empty() { + return (false, Vec::new()); + } + + let mut out = Vec::new(); + let mut pos = 0; + for &split in splits.iter() { + if pos + split >= width { + break; + } + + pos += split + 1; + out.push(split); + } + + let _ = splits.drain(..out.len()); + + if splits.is_empty() { + return (false, out); + } + + if pos <= width { + splits[0] -= width - pos; + if splits[0] > 0 { + splits[0] -= 1; + } else { + let _ = splits.remove(0); + return (true, out); + } + } + + (false, out) + } + + fn squash_splits(splits: &mut [usize]) { + splits.iter_mut().enumerate().for_each(|(i, s)| *s += i); + } + + fn set_margin(table: &str, margin: Sides<Indent>, color: Sides<Option<StaticColor>>) -> String { + if table.is_empty() { + return String::new(); + } + + let mut buf = String::new(); + let width = string_width_multiline(table); + let top_color = color.top; + let bottom_color = color.bottom; + let left_color = color.left; + let right_color = color.right; + for _ in 0..margin.top.size { + print_chars(&mut buf, margin.left.fill, left_color, margin.left.size); + print_chars(&mut buf, margin.top.fill, top_color, width); + print_chars(&mut buf, margin.right.fill, right_color, margin.right.size); + buf.push('\n'); + } + + for line in get_lines(table) { + print_chars(&mut buf, margin.left.fill, left_color, margin.left.size); + buf.push_str(&line); + print_chars(&mut buf, margin.right.fill, right_color, margin.right.size); + buf.push('\n'); + } + + for _ in 0..margin.bottom.size { + print_chars(&mut buf, margin.left.fill, left_color, margin.left.size); + print_chars(&mut buf, margin.bottom.fill, bottom_color, width); + print_chars(&mut buf, margin.right.fill, right_color, margin.right.size); + buf.push('\n'); + } + + let _ = buf.remove(buf.len() - 1); + + buf + } + + fn convert_border_colors(pad_color: Sides<StaticColor>) -> Sides<Option<StaticColor>> { + Sides::new( + (!pad_color.left.is_empty()).then(|| pad_color.left), + (!pad_color.right.is_empty()).then(|| pad_color.right), + (!pad_color.top.is_empty()).then(|| pad_color.top), + (!pad_color.bottom.is_empty()).then(|| pad_color.bottom), + ) + } +} diff --git a/vendor/tabled/src/tables/util/mod.rs b/vendor/tabled/src/tables/util/mod.rs new file mode 100644 index 000000000..ea5453b8f --- /dev/null +++ b/vendor/tabled/src/tables/util/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "std")] +pub(crate) mod utf8_writer; diff --git a/vendor/tabled/src/tables/util/utf8_writer.rs b/vendor/tabled/src/tables/util/utf8_writer.rs new file mode 100644 index 000000000..d4fc515df --- /dev/null +++ b/vendor/tabled/src/tables/util/utf8_writer.rs @@ -0,0 +1,29 @@ +use std::fmt; +use std::io; + +pub(crate) struct UTF8Writer<W>(W); + +impl<W> UTF8Writer<W> { + pub(crate) fn new(writer: W) -> Self { + Self(writer) + } +} + +impl<W> fmt::Write for UTF8Writer<W> +where + W: io::Write, +{ + fn write_str(&mut self, s: &str) -> fmt::Result { + let mut buf = s.as_bytes(); + loop { + let n = self.0.write(buf).map_err(|_| fmt::Error)?; + if n == buf.len() { + break; + } + + buf = &buf[n..]; + } + + Ok(()) + } +} |