summaryrefslogtreecommitdiffstats
path: root/vendor/tabled/src/tables/extended.rs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/tabled/src/tables/extended.rs')
-rw-r--r--vendor/tabled/src/tables/extended.rs338
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)
+}