diff options
Diffstat (limited to 'vendor/tabled/src/tables/extended.rs')
-rw-r--r-- | vendor/tabled/src/tables/extended.rs | 338 |
1 files changed, 338 insertions, 0 deletions
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) +} |