papergrid-0.14.0/.cargo_vcs_info.json0000644000000001470000000000100130750ustar { "git": { "sha1": "1fe1ec5f6146b282eda4bae2a2133aa4e455bc21" }, "path_in_vcs": "papergrid" }papergrid-0.14.0/.gitignore000064400000000000000000000005001046102023000136460ustar 00000000000000# Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk papergrid-0.14.0/Cargo.lock0000644000000104700000000000100110500ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "ansi-str" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "060de1453b69f46304b28274f382132f4e72c55637cf362920926a70d090890d" dependencies = [ "ansitok 0.3.0", ] [[package]] name = "ansitok" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "220044e6a1bb31ddee4e3db724d29767f352de47445a6cd75e1a173142136c83" dependencies = [ "nom", "vte 0.10.1", ] [[package]] name = "ansitok" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0a8acea8c2f1c60f0a92a8cd26bf96ca97db56f10bbcab238bbe0cceba659ee" dependencies = [ "nom", "vte 0.14.1", ] [[package]] name = "arrayvec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "bytecount" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "papergrid" version = "0.14.0" dependencies = [ "ansi-str", "ansitok 0.3.0", "bytecount", "fnv", "testing_table", "unicode-width", ] [[package]] name = "proc-macro2" version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] [[package]] name = "testing_table" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9491a98ee2ccc17bd4178e29d9dace769bff6cdb5e876b57ab9ef4ef38edd9e" dependencies = [ "ansitok 0.2.0", "unicode-width", ] [[package]] name = "unicode-ident" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-width" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vte" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" dependencies = [ "arrayvec 0.5.2", "utf8parse", "vte_generate_state_changes", ] [[package]] name = "vte" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" dependencies = [ "arrayvec 0.7.6", "memchr", ] [[package]] name = "vte_generate_state_changes" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" dependencies = [ "proc-macro2", "quote", ] papergrid-0.14.0/Cargo.toml0000644000000037370000000000100111030ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "papergrid" version = "0.14.0" authors = ["Maxim Zhiburt "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Papergrid is a core library to print a table" readme = "README.md" license = "MIT" repository = "https://github.com/zhiburt/tabled" [lib] name = "papergrid" path = "src/lib.rs" [[example]] name = "color_map" path = "examples/color_map.rs" required-features = ["std"] [[example]] name = "colored_border" path = "examples/colored_border.rs" required-features = ["std"] [[example]] name = "common_grid" path = "examples/common_grid.rs" required-features = ["std"] [[example]] name = "common_grid_no_std" path = "examples/common_grid_no_std.rs" required-features = [] [[example]] name = "hello_world" path = "examples/hello_world.rs" required-features = ["std"] [[example]] name = "papergrid_color" path = "examples/papergrid_color.rs" required-features = [ "std", "ansi", ] [[example]] name = "span_usage" path = "examples/span_usage.rs" required-features = ["std"] [[test]] name = "main" path = "tests/main.rs" [dependencies.ansi-str] version = "0.9" optional = true [dependencies.ansitok] version = "0.3" optional = true [dependencies.bytecount] version = "0.6" [dependencies.fnv] version = "1.0" [dependencies.unicode-width] version = "0.2" [dev-dependencies.testing_table] version = "0.2" features = ["ansi"] [features] ansi = [ "ansi-str", "ansitok", ] default = ["std"] std = [] papergrid-0.14.0/Cargo.toml.orig000064400000000000000000000022631046102023000145550ustar 00000000000000[package] name = "papergrid" version = "0.14.0" authors = ["Maxim Zhiburt "] edition = "2018" description = "Papergrid is a core library to print a table" repository = "https://github.com/zhiburt/tabled" license = "MIT" [features] default = ["std"] std = [] ansi = ["ansi-str", "ansitok"] [dependencies] unicode-width = "0.2" bytecount = "0.6" fnv = "1.0" ansi-str = { version = "0.9", optional = true } ansitok = { version = "0.3", optional = true } [dev-dependencies] testing_table = { version = "0.2", features = ["ansi"] } [[example]] name = "papergrid_color" required-features = ["std", "ansi"] [[example]] name = "color_map" path = "examples/color_map.rs" required-features = ["std"] [[example]] name = "colored_border" path = "examples/colored_border.rs" required-features = ["std"] [[example]] name = "common_grid" path = "examples/common_grid.rs" required-features = ["std"] [[example]] name = "span_usage" path = "examples/span_usage.rs" required-features = ["std"] [[example]] name = "common_grid_no_std" path = "examples/common_grid_no_std.rs" required-features = [] [[example]] name = "hello_world" path = "examples/hello_world.rs" required-features = ["std"] papergrid-0.14.0/LICENSE-MIT000064400000000000000000000020561046102023000133220ustar 00000000000000MIT License Copyright (c) 2021 Maxim Zhiburt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. papergrid-0.14.0/README.md000064400000000000000000000044121046102023000131430ustar 00000000000000# papergrid This is library for pretty tables. It has relatively low level API. If you're interested in a more friendly one take a look at [`tabled`](https://github.com/zhiburt/tabled). ## Usage ```rust use papergrid::{ colors::NoColors, config::{ spanned::SpannedConfig, AlignmentHorizontal, AlignmentVertical, Borders, Entity, Indent, Sides, }, dimension::{spanned::SpannedGridDimension, Estimate}, grid::peekable::PeekableGrid, records::vec_records::{CellInfo, VecRecords}, }; fn main() { let mut cfg = SpannedConfig::default(); cfg.set_borders(Borders { top: Some('-'), top_left: Some('+'), top_right: Some('+'), top_intersection: Some('+'), bottom: Some('-'), bottom_left: Some('+'), bottom_right: Some('+'), bottom_intersection: Some('+'), horizontal: Some('-'), left_intersection: Some('+'), right_intersection: Some('+'), vertical: Some('|'), left: Some('|'), right: Some('|'), intersection: Some('+'), }); cfg.set_column_span((1, 1), 3); cfg.set_row_span((0, 0), 2); cfg.set_alignment_horizontal((1, 0).into(), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Entity::Global, AlignmentVertical::Center); cfg.set_padding( (0, 0).into(), Sides::new( Indent::spaced(4), Indent::spaced(4), Indent::spaced(1), Indent::spaced(1), ), ); let data = [ ["Papergrid", "is a library", "for print tables", "!"], ["", "Just like this", "", ""], ]; let data = data .iter() .map(|row| row.iter().map(CellInfo::new).collect()) .collect(); let records = VecRecords::new(data); let mut dims = SpannedGridDimension::default(); dims.estimate(&records, &cfg); let grid = PeekableGrid::new(&records, &cfg, &dims, NoColors).to_string(); println!("{grid}"); } ``` Running the example you must see. ```text +-----------------+------------+----------------+-+ | |is a library|for print tables|!| + Papergrid +------------+----------------+-+ | |Just like this | +-----------------+------------+----------------+-+ ```papergrid-0.14.0/examples/color_map.rs000064400000000000000000000044321046102023000160250ustar 00000000000000//! This example demonstrates using a [`HashMap`] of colors to simplify styling //! sections of a [`Grid`] without embedding ANSI escape characters into cell values. //! //! * 🚩 This example requires the `ansi` feature. use std::{ collections::HashMap, fmt::{self}, io::{self, Write}, }; use papergrid::{ ansi::ANSIFmt, config::{pos, spanned::SpannedConfig, Borders, Position}, dimension::{spanned::SpannedGridDimension, Estimate}, grid::iterable::Grid, records::IterRecords, }; fn main() { let records = vec![vec!["Hello", "World"], vec!["Hi", "World"]]; let records = IterRecords::new(&records, 2, None); let cfg = generate_table_config(); let mut dims = SpannedGridDimension::default(); dims.estimate(records, &cfg); let colors = generate_colors(); let grid = Grid::new(records, &dims, &cfg, &colors); grid.build(StdoutWriter::new()).unwrap(); println!(); } fn generate_colors() -> HashMap { let mut m = HashMap::default(); m.insert(pos(0, 0), Style::Blue); m.insert(pos(1, 1), Style::Black); m } fn generate_table_config() -> SpannedConfig { let mut cfg = SpannedConfig::default(); cfg.set_borders(Borders { top: Some('-'), bottom: Some('-'), left: Some('|'), right: Some('|'), vertical: Some('|'), horizontal: Some('-'), ..Default::default() }); cfg.set_borders_missing('+'); cfg } #[derive(Debug, Clone, Copy)] enum Style { Black, Blue, } impl ANSIFmt for Style { fn fmt_ansi_prefix(&self, f: &mut W) -> fmt::Result { let c = match self { Style::Black => "\u{1b}[34m", Style::Blue => "\u{1b}[31m", }; f.write_str(c) } } struct StdoutWriter(io::Stdout); impl StdoutWriter { fn new() -> Self { Self(io::stdout()) } } impl fmt::Write for StdoutWriter { fn write_str(&mut self, s: &str) -> fmt::Result { let buf = s.as_bytes(); let mut offset = 0; loop { let n = self.0.write(&buf[offset..]).map_err(|_| fmt::Error)?; if n == buf.len() { break; } offset += n; } self.0.flush().map_err(|_| fmt::Error)?; Ok(()) } } papergrid-0.14.0/examples/colored_border.rs000064400000000000000000000043211046102023000170330ustar 00000000000000//! This example demonstrates using colors to stylize [`Grid`] borders. //! Borders can be set globally with [`SpannedConfig::set_border_color_global()`] //! or individually with [`SpannedConfig::set_border_color()`]. //! //! * 🚩 This example requires the `color` feature. //! //! * [`CompactConfig`] also supports colorization when the `color` feature is enabled. use papergrid::{ ansi::ANSIBuf, colors::NoColors, config::{ pos, spanned::SpannedConfig, AlignmentHorizontal, AlignmentVertical, Borders, Entity::Global, Indent, Sides, }, dimension::{spanned::SpannedGridDimension, Estimate}, grid::iterable::Grid, records::IterRecords, }; fn main() { let cfg = generate_table_config(); let data = vec![ vec!["Papergrid", "is a library", "for printing tables", "!"], vec!["", "Just like this", "", ""], ]; let records = IterRecords::new(data, 4, Some(2)); let mut dim = SpannedGridDimension::default(); dim.estimate(&records, &cfg); let grid = Grid::new(records, &dim, &cfg, NoColors).to_string(); println!("{grid}"); } fn generate_table_config() -> SpannedConfig { let style = Borders { top: Some('-'), top_left: Some('+'), top_right: Some('+'), top_intersection: Some('+'), bottom: Some('-'), bottom_left: Some('+'), bottom_right: Some('+'), bottom_intersection: Some('+'), horizontal: Some('-'), left_intersection: Some('+'), right_intersection: Some('+'), vertical: Some('|'), left: Some('|'), right: Some('|'), intersection: Some('+'), }; let mut cfg = SpannedConfig::default(); cfg.set_borders(style); cfg.set_column_span(pos(1, 1), 3); cfg.set_row_span(pos(0, 0), 2); cfg.set_alignment_horizontal((1, 0).into(), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Global, AlignmentVertical::Center); cfg.set_padding( (0, 0).into(), Sides::new( Indent::spaced(4), Indent::spaced(4), Indent::spaced(1), Indent::spaced(1), ), ); cfg.set_border_color_default(ANSIBuf::new("\u{1b}[42m", "\u{1b}[0m")); cfg } papergrid-0.14.0/examples/common_grid.rs000064400000000000000000000037221046102023000163500ustar 00000000000000//! This example demonstrates the flexibility of [`papergrid`] with manual configurations //! of [`Borders`], [`CompactConfig`], and column counts with [`IterRecords`]. //! //! * For an alternative to [`CompactGrid`] and [`CompactGridDimension`] with //! flexible row height, variable intra-column spans, and multiline cell support //! see [`Grid`] and [`SpannedGridDimension`]. use papergrid::{ config::compact::CompactConfig, config::{AlignmentHorizontal, Borders, Indent, Sides}, dimension::compact::CompactGridDimension, dimension::Estimate, grid::compact::CompactGrid, records::IterRecords, }; fn main() { let cfg = generate_table_config(); let data = [ ["Papergrid", "is a library", "for printing tables", "!"], [ "Just like this", "NOTICE", "that multiline is not supported", "H\ne\nl\nl\no", ], ]; let records = IterRecords::new(data, 4, None); let mut dim = CompactGridDimension::default(); dim.estimate(records, &cfg); let grid = CompactGrid::new(records, &dim, &cfg); println!("{grid}"); } const fn generate_table_config() -> CompactConfig { const STYLE: Borders = Borders { top: Some('-'), top_left: Some('+'), top_right: Some('+'), top_intersection: Some('+'), bottom: Some('-'), bottom_left: Some('+'), bottom_right: Some('+'), bottom_intersection: Some('+'), horizontal: Some('-'), left_intersection: Some('+'), right_intersection: Some('+'), vertical: Some('|'), left: Some('|'), right: Some('|'), intersection: Some('+'), }; CompactConfig::new() .set_borders(STYLE) .set_alignment_horizontal(AlignmentHorizontal::Center) .set_padding(Sides::new( Indent::spaced(1), Indent::spaced(1), Indent::spaced(0), Indent::spaced(0), )) } papergrid-0.14.0/examples/common_grid_no_std.rs000064400000000000000000000036341046102023000177200ustar 00000000000000//! This example demonstrates using [`papergrid`] without [The Rust Standard Library](std). //! //! * Note the missing, pre-built [`Dimension`] implementations requiring manual design. use papergrid::{ config::compact::CompactConfig, config::{AlignmentHorizontal, Borders, Indent, Sides}, dimension::Dimension, grid::compact::CompactGrid, records::IterRecords, }; fn main() { let data = [ ["Papergrid", "is a library", "for printing tables", "!"], [ "Just like this", "NOTICE", "that multiline is not supported", "H\ne\nl\nl\no", ], ]; let records = IterRecords::new(data, 4, None); let dim = ConstDims(&[20, 15, 40, 3], 4); let cfg = generate_table_config(); let grid = CompactGrid::new(records, &dim, &cfg); println!("{grid}"); } const fn generate_table_config() -> CompactConfig { const STYLE: Borders = Borders { top: Some('-'), top_left: Some('+'), top_right: Some('+'), top_intersection: Some('+'), bottom: Some('-'), bottom_left: Some('+'), bottom_right: Some('+'), bottom_intersection: Some('+'), horizontal: Some('-'), left_intersection: Some('+'), right_intersection: Some('+'), vertical: Some('|'), left: Some('|'), right: Some('|'), intersection: Some('+'), }; CompactConfig::new() .set_borders(STYLE) .set_alignment_horizontal(AlignmentHorizontal::Center) .set_padding(Sides::new( Indent::spaced(1), Indent::spaced(1), Indent::spaced(3), Indent::spaced(0), )) } struct ConstDims<'a>(&'a [usize], usize); impl Dimension for ConstDims<'_> { fn get_width(&self, column: usize) -> usize { self.0[column] } fn get_height(&self, _: usize) -> usize { self.1 } } papergrid-0.14.0/examples/hello_world.rs000064400000000000000000000046771046102023000163770ustar 00000000000000//! This example demonstrates the flexibility of [`papergrid`] with manual configurations //! of [`Borders`], [`SpannedConfig`], and column counts with [`IterRecords`]. //! //! * For an alternative to [`Grid`] and [`SpannedGridDimension`] with //! uniform row height and intra-column spans see [`CompactGrid`] and [`CompactGridDimension`]. //! * Note that [`Grid`] supports multiline cells whereas [`CompactGrid`] does not. //! * Note that [`Dimension`] implementations rely on [`Dimension::estimate()`] //! to correctly format outputs, and typically trigger index-out-of-bounds errors otherwise. use papergrid::{ colors::NoColors, config::{ pos, spanned::SpannedConfig, AlignmentHorizontal, AlignmentVertical, Borders, Entity::Global, Indent, Sides, }, dimension::{spanned::SpannedGridDimension, Estimate}, grid::iterable::Grid, records::IterRecords, }; fn main() { let cfg = generate_table_config(); let data = [ ["Papergrid", "is a library", "for printing tables", "!"], [ "", "Just like\n\nthis", "", "", ], ]; let records = IterRecords::new(data, 4, None); let mut dim = SpannedGridDimension::default(); dim.estimate(records, &cfg); let grid = Grid::new(records, &dim, &cfg, NoColors).to_string(); println!("{grid}"); } fn generate_table_config() -> SpannedConfig { const STYLE: Borders = Borders { top: Some('-'), top_left: Some('+'), top_right: Some('+'), top_intersection: Some('+'), bottom: Some('-'), bottom_left: Some('+'), bottom_right: Some('+'), bottom_intersection: Some('+'), horizontal: Some('-'), left_intersection: Some('+'), right_intersection: Some('+'), vertical: Some('|'), left: Some('|'), right: Some('|'), intersection: Some('+'), }; let mut cfg = SpannedConfig::default(); cfg.set_borders(STYLE); cfg.set_column_span(pos(1, 1), 3); cfg.set_row_span(pos(0, 0), 2); cfg.set_alignment_horizontal((1, 0).into(), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Global, AlignmentVertical::Center); cfg.set_padding( (0, 0).into(), Sides::new( Indent::spaced(4), Indent::spaced(4), Indent::spaced(1), Indent::spaced(1), ), ); cfg } papergrid-0.14.0/examples/papergrid_color.rs000064400000000000000000000035361046102023000172310ustar 00000000000000//! This example demonstrates using colors to stylize [`Grid`] cells. //! //! * 🚩 This example requires the `color` feature. //! //! * Note that this example uses inline ANSI escape characters to style //! grid cells. `Grid::new(_, _, _, NoColors)` indicates that a color //! map is not provided. NOT that colors are ignored in the output. use std::{ fmt, io::{self, Write}, }; use papergrid::{ colors::NoColors, config::spanned::SpannedConfig, config::Borders, dimension::spanned::SpannedGridDimension, dimension::Estimate, grid::iterable::Grid, records::IterRecords, }; fn main() { let data = vec![ vec!["\u{1b}[42mHello\u{1b}[0m", "\u{1b}[43mWorld\u{1b}[0m"], vec!["\u{1b}[44mHi\u{1b}[0m", "\u{1b}[45mWorld\u{1b}[0m"], ]; let records = IterRecords::new(data, 2, None); let cfg = generate_table_config(); let mut dimension = SpannedGridDimension::default(); dimension.estimate(&records, &cfg); let grid = Grid::new(&records, &dimension, &cfg, NoColors); grid.build(UTF8Stdout(io::stdout())).unwrap(); println!(); } fn generate_table_config() -> SpannedConfig { let mut cfg = SpannedConfig::default(); cfg.set_borders(Borders { top: Some('-'), bottom: Some('-'), left: Some('|'), right: Some('|'), vertical: Some('|'), horizontal: Some('-'), ..Default::default() }); cfg.set_borders_missing('+'); cfg } struct UTF8Stdout(io::Stdout); impl fmt::Write for UTF8Stdout { 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..]; } self.0.flush().map_err(|_| fmt::Error)?; Ok(()) } } papergrid-0.14.0/examples/span_usage.rs000064400000000000000000000034071046102023000162000ustar 00000000000000use papergrid::{ colors::NoColors, config::{ pos, spanned::SpannedConfig, AlignmentHorizontal, AlignmentVertical, Borders, Entity, Indent, Sides, }, dimension::{spanned::SpannedGridDimension, Estimate}, grid::peekable::PeekableGrid, records::vec_records::{Text, VecRecords}, }; fn main() { let mut cfg = SpannedConfig::default(); cfg.set_borders(Borders { top: Some('-'), top_left: Some('+'), top_right: Some('+'), top_intersection: Some('+'), bottom: Some('-'), bottom_left: Some('+'), bottom_right: Some('+'), bottom_intersection: Some('+'), horizontal: Some('-'), left_intersection: Some('+'), right_intersection: Some('+'), vertical: Some('|'), left: Some('|'), right: Some('|'), intersection: Some('+'), }); cfg.set_column_span(pos(1, 1), 3); cfg.set_row_span(pos(0, 0), 2); cfg.set_alignment_horizontal((1, 0).into(), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Entity::Global, AlignmentVertical::Center); cfg.set_padding( (0, 0).into(), Sides::new( Indent::spaced(4), Indent::spaced(4), Indent::spaced(1), Indent::spaced(1), ), ); let data = [ ["Papergrid", "is a library", "for print tables", "!"], ["", "Just like this", "", ""], ]; let data = data .iter() .map(|row| row.iter().map(Text::new).collect()) .collect(); let records = VecRecords::new(data); let mut dims = SpannedGridDimension::default(); dims.estimate(&records, &cfg); let grid = PeekableGrid::new(&records, &cfg, &dims, NoColors).to_string(); println!("{grid}"); } papergrid-0.14.0/src/ansi/ansi_buf.rs000064400000000000000000000044731046102023000155500ustar 00000000000000use std::fmt::{self, Write}; use super::{ANSIFmt, ANSIStr}; /// The structure represents a ANSI color by suffix and prefix. #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ANSIBuf { prefix: String, suffix: String, } impl ANSIBuf { /// Constructs a new instance with suffix and prefix. /// /// They are not checked so you should make sure you provide correct ANSI. /// Otherwise you may want to use [`TryFrom`]. /// /// [`TryFrom`]: std::convert::TryFrom pub fn new(prefix: P, suffix: S) -> Self where P: Into, S: Into, { let prefix = prefix.into(); let suffix = suffix.into(); Self { prefix, suffix } } /// Checks whether the color is not actually set. pub fn is_empty(&self) -> bool { self.prefix.is_empty() && self.suffix.is_empty() } /// Gets a reference to a prefix. pub fn get_prefix(&self) -> &str { &self.prefix } /// Gets a reference to a suffix. pub fn get_suffix(&self) -> &str { &self.suffix } /// Gets a reference as a color. pub fn as_ref(&self) -> ANSIStr<'_> { ANSIStr::new(&self.prefix, &self.suffix) } } impl ANSIFmt for ANSIBuf { fn fmt_ansi_prefix(&self, f: &mut W) -> fmt::Result { f.write_str(&self.prefix) } fn fmt_ansi_suffix(&self, f: &mut W) -> fmt::Result { f.write_str(&self.suffix) } } #[cfg(feature = "ansi")] impl std::convert::TryFrom<&str> for ANSIBuf { type Error = (); fn try_from(value: &str) -> Result { parse_ansi_color(value).ok_or(()) } } #[cfg(feature = "ansi")] impl std::convert::TryFrom for ANSIBuf { type Error = (); fn try_from(value: String) -> Result { Self::try_from(value.as_str()) } } #[cfg(feature = "ansi")] fn parse_ansi_color(s: &str) -> Option { let mut blocks = ansi_str::get_blocks(s); let block = blocks.next()?; let style = block.style(); let start = style.start().to_string(); let end = style.end().to_string(); Some(ANSIBuf::new(start, end)) } impl From> for ANSIBuf { fn from(value: ANSIStr<'_>) -> Self { Self::new(value.get_prefix(), value.get_suffix()) } } papergrid-0.14.0/src/ansi/ansi_str.rs000064400000000000000000000026061046102023000156000ustar 00000000000000use core::fmt::{self, Write}; use super::ANSIFmt; /// The structure represents a ANSI color by suffix and prefix. #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct ANSIStr<'a> { prefix: &'a str, suffix: &'a str, } impl<'a> ANSIStr<'a> { /// Constructs a new instance with suffix and prefix. /// /// They are not checked so you should make sure you provide correct ANSI. /// Otherwise you may want to use [`TryFrom`]. /// /// [`TryFrom`]: std::convert::TryFrom pub const fn new(prefix: &'a str, suffix: &'a str) -> Self { Self { prefix, suffix } } /// Constructs a new instance with suffix and prefix set to empty strings. pub const fn empty() -> Self { Self::new("", "") } /// Verifies if anything was actually set. pub const fn is_empty(&self) -> bool { self.prefix.is_empty() && self.suffix.is_empty() } /// Gets a reference to a prefix. pub const fn get_prefix(&self) -> &'a str { self.prefix } /// Gets a reference to a suffix. pub const fn get_suffix(&self) -> &'a str { self.suffix } } impl ANSIFmt for ANSIStr<'_> { fn fmt_ansi_prefix(&self, f: &mut W) -> fmt::Result { f.write_str(self.prefix) } fn fmt_ansi_suffix(&self, f: &mut W) -> fmt::Result { f.write_str(self.suffix) } } papergrid-0.14.0/src/ansi/mod.rs000064400000000000000000000015601046102023000145330ustar 00000000000000//! A module which contains [`ANSIFmt`] trait and its implementation [`ANSIStr`] #[cfg_attr(feature = "std", doc = "and [`ANSIBuf`].")] #[cfg(feature = "std")] mod ansi_buf; mod ansi_str; #[cfg(feature = "std")] pub use ansi_buf::ANSIBuf; pub use self::ansi_str::ANSIStr; use core::fmt::{self, Write}; /// A trait which prints an ANSI prefix and suffix. pub trait ANSIFmt { /// Print ANSI prefix. fn fmt_ansi_prefix(&self, f: &mut W) -> fmt::Result; /// Print ANSI suffix. fn fmt_ansi_suffix(&self, f: &mut W) -> fmt::Result { f.write_str("\u{1b}[0m") } } impl ANSIFmt for &C where C: ANSIFmt, { fn fmt_ansi_prefix(&self, f: &mut W) -> fmt::Result { C::fmt_ansi_prefix(self, f) } fn fmt_ansi_suffix(&self, f: &mut W) -> fmt::Result { C::fmt_ansi_suffix(self, f) } } papergrid-0.14.0/src/colors.rs000064400000000000000000000043601046102023000143240ustar 00000000000000//! A module which contains [Colors] trait and its blanket implementations. use crate::{ansi::ANSIFmt, config::Position}; /// A trait which represents map of colors. pub trait Colors { /// Color implementation. type Color: ANSIFmt; /// Returns a color for a given position. fn get_color(&self, pos: Position) -> Option<&Self::Color>; /// Verifies whether a map is empty or not. fn is_empty(&self) -> bool; } impl Colors for &'_ C where C: Colors, { type Color = C::Color; fn get_color(&self, pos: Position) -> Option<&Self::Color> { C::get_color(self, pos) } fn is_empty(&self) -> bool { C::is_empty(self) } } #[cfg(feature = "std")] impl Colors for std::collections::HashMap where C: ANSIFmt, { type Color = C; fn get_color(&self, pos: Position) -> Option<&Self::Color> { self.get(&pos) } fn is_empty(&self) -> bool { std::collections::HashMap::is_empty(self) } } #[cfg(feature = "std")] impl Colors for std::collections::BTreeMap where C: ANSIFmt, { type Color = C; fn get_color(&self, pos: Position) -> Option<&Self::Color> { self.get(&pos) } fn is_empty(&self) -> bool { std::collections::BTreeMap::is_empty(self) } } #[cfg(feature = "std")] impl Colors for crate::config::spanned::EntityMap> where C: ANSIFmt + PartialEq, { type Color = C; fn get_color(&self, pos: Position) -> Option<&Self::Color> { self.get(pos).as_ref() } fn is_empty(&self) -> bool { self.is_empty() && self.as_ref().is_none() } } /// The structure represents empty [`Colors`] map. #[derive(Debug, Default, Clone)] pub struct NoColors; impl Colors for NoColors { type Color = EmptyColor; fn get_color(&self, _: Position) -> Option<&Self::Color> { None } fn is_empty(&self) -> bool { true } } /// A color which is actually has not value. #[derive(Debug)] pub struct EmptyColor; impl ANSIFmt for EmptyColor { fn fmt_ansi_prefix(&self, _: &mut W) -> core::fmt::Result { Ok(()) } fn fmt_ansi_suffix(&self, _: &mut W) -> core::fmt::Result { Ok(()) } } papergrid-0.14.0/src/config/alignment.rs000064400000000000000000000011231046102023000162400ustar 00000000000000/// [`AlignmentHorizontal`] represents an horizontal alignment of a cell content. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub enum AlignmentHorizontal { /// Align to the center. Center, /// Align on the left. Left, /// Align on the right. Right, } /// [`AlignmentVertical`] represents an vertical alignment of a cell content. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub enum AlignmentVertical { /// Align to the center. Center, /// Align to the top. Top, /// Align to the bottom. Bottom, } papergrid-0.14.0/src/config/border.rs000064400000000000000000000161421046102023000155460ustar 00000000000000/// Border is a representation of a cells's borders (left, right, top, bottom, and the corners) /// /// /// ```text /// top border /// | /// V /// corner top left ------> +_______+ <---- corner top left /// | | /// left border ----------> | cell | <---- right border /// | | /// corner bottom right --> +_______+ <---- corner bottom right /// ^ /// | /// bottom border /// ``` #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, PartialOrd, Ord)] pub struct Border { /// A character for a top. pub top: Option, /// A character for a bottom. pub bottom: Option, /// A character for a left. pub left: Option, /// A character for a right. pub right: Option, /// A character for a left top corner. pub left_top_corner: Option, /// A character for a left bottom corner. pub left_bottom_corner: Option, /// A character for a right top corner. pub right_top_corner: Option, /// A character for a right bottom corner. pub right_bottom_corner: Option, } impl Border { /// This function constructs a cell borders with all sides set. #[allow(clippy::too_many_arguments)] pub const fn new( top: Option, bottom: Option, left: Option, right: Option, left_top_corner: Option, left_bottom_corner: Option, right_top_corner: Option, right_bottom_corner: Option, ) -> Self { Self { top, bottom, left, right, left_top_corner, left_bottom_corner, right_top_corner, right_bottom_corner, } } /// This function constructs a cell borders with all sides set. #[allow(clippy::too_many_arguments)] pub const fn full( top: T, bottom: T, left: T, right: T, top_left: T, top_right: T, bottom_left: T, bottom_right: T, ) -> Self { Self::new( Some(top), Some(bottom), Some(left), Some(right), Some(top_left), Some(bottom_left), Some(top_right), Some(bottom_right), ) } /// This function constructs a cell borders with all sides being empty (set off). pub const fn empty() -> Self { Self::new(None, None, None, None, None, None, None, None) } /// Checks whether any side is set. pub const fn is_empty(&self) -> bool { self.top.is_none() && self.left_top_corner.is_none() && self.right_top_corner.is_none() && self.bottom.is_none() && self.left_bottom_corner.is_none() && self.left_top_corner.is_none() && self.left.is_none() && self.right.is_none() } /// Checks whether all sides are equal to one another. pub fn is_same(&self) -> bool where T: PartialEq, { self.top == self.bottom && self.top == self.left && self.top == self.right && self.top == self.left_top_corner && self.top == self.right_top_corner && self.top == self.left_bottom_corner && self.top == self.right_bottom_corner } /// Verifies whether anything is set on the top. pub const fn has_top(&self) -> bool { self.top.is_some() || self.left_top_corner.is_some() || self.right_top_corner.is_some() } /// Verifies whether anything is set on the bottom. pub const fn has_bottom(&self) -> bool { self.bottom.is_some() || self.left_bottom_corner.is_some() || self.right_bottom_corner.is_some() } /// Verifies whether anything is set on the left. pub const fn has_left(&self) -> bool { self.left.is_some() || self.left_top_corner.is_some() || self.left_bottom_corner.is_some() } /// Verifies whether anything is set on the right. pub const fn has_right(&self) -> bool { self.right.is_some() || self.right_top_corner.is_some() || self.right_bottom_corner.is_some() } /// Converts a border with a given function. pub fn map(self, f: F) -> Border where F: Fn(T) -> T1, { Border { top: self.top.map(&f), bottom: self.bottom.map(&f), left: self.left.map(&f), right: self.right.map(&f), left_top_corner: self.left_top_corner.map(&f), left_bottom_corner: self.left_bottom_corner.map(&f), right_top_corner: self.right_top_corner.map(&f), right_bottom_corner: self.right_bottom_corner.map(&f), } } } impl Border { /// This function constructs a cell borders with all sides's char set to a given character. /// /// It behaves like [`Border::full`] with the same character set to each side. pub const fn filled(c: T) -> Self { Self::full(c, c, c, c, c, c, c, c) } } impl Border<&T> { /// Copies the underlying reference to a new border. pub fn copied(&self) -> Border { Border { top: self.top.copied(), bottom: self.bottom.copied(), left: self.left.copied(), right: self.right.copied(), left_bottom_corner: self.left_bottom_corner.copied(), left_top_corner: self.left_top_corner.copied(), right_bottom_corner: self.right_bottom_corner.copied(), right_top_corner: self.right_top_corner.copied(), } } } impl Border<&T> { /// Copies the underlying reference to a new border. pub fn cloned(&self) -> Border { Border { top: self.top.cloned(), bottom: self.bottom.cloned(), left: self.left.cloned(), right: self.right.cloned(), left_bottom_corner: self.left_bottom_corner.cloned(), left_top_corner: self.left_top_corner.cloned(), right_bottom_corner: self.right_bottom_corner.cloned(), right_top_corner: self.right_top_corner.cloned(), } } } impl Border { /// Convert all values on the border into another ones. pub fn convert(self) -> Border where B: From, { macro_rules! conv_opt { ($opt:expr) => { match $opt { Some(opt) => Some(B::from(opt)), None => None, } }; } Border { top: conv_opt!(self.top), bottom: conv_opt!(self.bottom), left: conv_opt!(self.left), right: conv_opt!(self.right), left_bottom_corner: conv_opt!(self.left_bottom_corner), left_top_corner: conv_opt!(self.left_top_corner), right_bottom_corner: conv_opt!(self.right_bottom_corner), right_top_corner: conv_opt!(self.right_top_corner), } } } papergrid-0.14.0/src/config/borders.rs000064400000000000000000000153421046102023000157320ustar 00000000000000/// Borders represents a Table frame with horizontal and vertical split lines. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Borders { /// A top horizontal on the frame. pub top: Option, /// A top left on the frame. pub top_left: Option, /// A top right on the frame. pub top_right: Option, /// A top horizontal intersection on the frame. pub top_intersection: Option, /// A bottom horizontal on the frame. pub bottom: Option, /// A bottom left on the frame. pub bottom_left: Option, /// A bottom right on the frame. pub bottom_right: Option, /// A bottom horizontal intersection on the frame. pub bottom_intersection: Option, /// A horizontal split. pub horizontal: Option, /// A vertical split. pub vertical: Option, /// A top left character on the frame. pub intersection: Option, /// A vertical split on the left frame line. pub left: Option, /// A horizontal split on the left frame line. pub left_intersection: Option, /// A vertical split on the right frame line. pub right: Option, /// A horizontal split on the right frame line. pub right_intersection: Option, } impl Borders { /// Returns empty borders. pub const fn empty() -> Self { Self { top: None, top_left: None, top_right: None, top_intersection: None, bottom: None, bottom_left: None, bottom_right: None, bottom_intersection: None, horizontal: None, left: None, right: None, vertical: None, left_intersection: None, right_intersection: None, intersection: None, } } /// Returns Borders filled in with a supplied value. pub const fn filled(val: T) -> Self where T: Copy, { Self { top: Some(val), top_left: Some(val), top_right: Some(val), top_intersection: Some(val), bottom: Some(val), bottom_left: Some(val), bottom_right: Some(val), bottom_intersection: Some(val), horizontal: Some(val), left: Some(val), right: Some(val), vertical: Some(val), left_intersection: Some(val), right_intersection: Some(val), intersection: Some(val), } } /// A verification whether any border was set. pub const fn is_empty(&self) -> bool { self.top.is_none() && self.top_left.is_none() && self.top_right.is_none() && self.top_intersection.is_none() && self.bottom.is_none() && self.bottom_left.is_none() && self.bottom_right.is_none() && self.bottom_intersection.is_none() && self.horizontal.is_none() && self.left.is_none() && self.right.is_none() && self.vertical.is_none() && self.left_intersection.is_none() && self.right_intersection.is_none() && self.intersection.is_none() } /// Verifies if borders has left line set on the frame. pub const fn has_left(&self) -> bool { self.left.is_some() || self.left_intersection.is_some() || self.top_left.is_some() || self.bottom_left.is_some() } /// Verifies if borders has right line set on the frame. pub const fn has_right(&self) -> bool { self.right.is_some() || self.right_intersection.is_some() || self.top_right.is_some() || self.bottom_right.is_some() } /// Verifies if borders has top line set on the frame. pub const fn has_top(&self) -> bool { self.top.is_some() || self.top_intersection.is_some() || self.top_left.is_some() || self.top_right.is_some() } /// Verifies if borders has bottom line set on the frame. pub const fn has_bottom(&self) -> bool { self.bottom.is_some() || self.bottom_intersection.is_some() || self.bottom_left.is_some() || self.bottom_right.is_some() } /// Verifies if borders has horizontal lines set. pub const fn has_horizontal(&self) -> bool { self.horizontal.is_some() || self.left_intersection.is_some() || self.right_intersection.is_some() || self.intersection.is_some() } /// Verifies if borders has vertical lines set. pub const fn has_vertical(&self) -> bool { self.intersection.is_some() || self.vertical.is_some() || self.top_intersection.is_some() || self.bottom_intersection.is_some() } /// Converts borders type into another one. pub fn convert_into(self) -> Borders where T1: From, { Borders { left: self.left.map(Into::into), right: self.right.map(Into::into), top: self.top.map(Into::into), bottom: self.bottom.map(Into::into), bottom_intersection: self.bottom_intersection.map(Into::into), bottom_left: self.bottom_left.map(Into::into), bottom_right: self.bottom_right.map(Into::into), horizontal: self.horizontal.map(Into::into), intersection: self.intersection.map(Into::into), left_intersection: self.left_intersection.map(Into::into), right_intersection: self.right_intersection.map(Into::into), top_intersection: self.top_intersection.map(Into::into), top_left: self.top_left.map(Into::into), top_right: self.top_right.map(Into::into), vertical: self.vertical.map(Into::into), } } /// Converts borders with a given function. pub fn map(self, f: F) -> Borders where F: Fn(T) -> T1, { Borders { left: self.left.map(&f), right: self.right.map(&f), top: self.top.map(&f), bottom: self.bottom.map(&f), bottom_intersection: self.bottom_intersection.map(&f), bottom_left: self.bottom_left.map(&f), bottom_right: self.bottom_right.map(&f), horizontal: self.horizontal.map(&f), intersection: self.intersection.map(&f), left_intersection: self.left_intersection.map(&f), right_intersection: self.right_intersection.map(&f), top_intersection: self.top_intersection.map(&f), top_left: self.top_left.map(&f), top_right: self.top_right.map(&f), vertical: self.vertical.map(&f), } } } papergrid-0.14.0/src/config/compact/mod.rs000064400000000000000000000067651046102023000165100ustar 00000000000000//! A module which contains configuration of a [`CompactGrid`] which is responsible for grid configuration. //! //! [`CompactGrid`]: crate::grid::compact::CompactGrid use crate::ansi::ANSIStr; use crate::config::{AlignmentHorizontal, Borders, Indent, Sides}; /// This structure represents a settings of a grid. /// /// grid: crate::Grid. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct CompactConfig { borders: Borders, border_colors: Borders>, margin: Sides, margin_color: Sides>, padding: Sides, padding_color: Sides>, halignment: AlignmentHorizontal, } impl Default for CompactConfig { fn default() -> Self { Self::new() } } impl CompactConfig { /// Returns an standard config. pub const fn new() -> Self { Self { halignment: AlignmentHorizontal::Left, borders: Borders::empty(), border_colors: Borders::empty(), margin: Sides::filled(Indent::zero()), margin_color: Sides::filled(ANSIStr::new("", "")), padding: Sides::new( Indent::spaced(1), Indent::spaced(1), Indent::zero(), Indent::zero(), ), padding_color: Sides::filled(ANSIStr::new("", "")), } } /// Set grid margin. pub const fn set_margin(mut self, margin: Sides) -> Self { self.margin = margin; self } /// Returns a grid margin. pub const fn get_margin(&self) -> &Sides { &self.margin } /// Set the [`Borders`] value as correct one. pub const fn set_borders(mut self, borders: Borders) -> Self { self.borders = borders; self } /// Returns a current [`Borders`] structure. pub const fn get_borders(&self) -> &Borders { &self.borders } /// Returns a current [`Borders`] structure. pub const fn get_borders_color(&self) -> &Borders> { &self.border_colors } /// Set a padding to a given cells. pub const fn set_padding(mut self, padding: Sides) -> Self { self.padding = padding; self } /// Get a padding for a given. pub const fn get_padding(&self) -> &Sides { &self.padding } /// Set a horizontal alignment. pub const fn set_alignment_horizontal(mut self, alignment: AlignmentHorizontal) -> Self { self.halignment = alignment; self } /// Get a alignment horizontal. pub const fn get_alignment_horizontal(&self) -> AlignmentHorizontal { self.halignment } /// Sets colors of border carcass on the grid. pub const fn set_borders_color(mut self, borders: Borders>) -> Self { self.border_colors = borders; self } /// Set colors for a margin. pub const fn set_margin_color(mut self, color: Sides>) -> Self { self.margin_color = color; self } /// Returns a margin color. pub const fn get_margin_color(&self) -> &Sides> { &self.margin_color } /// Set a padding to a given cells. pub const fn set_padding_color(mut self, color: Sides>) -> Self { self.padding_color = color; self } /// Set a padding to a given cells. pub const fn get_padding_color(&self) -> &Sides> { &self.padding_color } } papergrid-0.14.0/src/config/entity.rs000064400000000000000000000155331046102023000156100ustar 00000000000000use super::Position; /// Entity a structure which represent a set of cells. /// /// For example such table: /// /// ```text /// ┌───┬───┐ /// │ 0 │ 1 │ /// ├───┌──── /// │ 1 │ 2 │ /// └───┮───┘ /// ``` /// /// - has 4 cells. /// Which indexes are (0, 0), (0, 1), (1, 0), (1, 1). /// /// - has 2 rows. /// Which indexes are 0, 1. /// /// - has 2 column. /// Which indexes are 0, 1. /// /// In [`Entity`] terms, all cells on the grid we call `Global`. #[derive(PartialEq, Eq, Debug, Hash, Clone, Copy)] pub enum Entity { /// All cells on the grid. Global, /// All cells in a column on the grid. Column(usize), /// All cells in a row on the grid. Row(usize), /// A particular cell (row, column) on the grid. Cell(usize, usize), } impl Entity { /// Iterate over cells which are covered via the [`Entity`]. pub fn iter(&self, count_rows: usize, count_cols: usize) -> EntityIterator { EntityIterator::new(*self, count_rows, count_cols) } } impl From for Entity { fn from(pos: Position) -> Self { Self::Cell(pos.row(), pos.col()) } } impl From<(usize, usize)> for Entity { fn from(pos: (usize, usize)) -> Self { Self::Cell(pos.0, pos.1) } } /// An iterator over cells. /// /// Produced from [`Entity::iter`]. #[derive(Debug, Clone)] pub struct EntityIterator { entity: Entity, count_rows: usize, count_cols: usize, i: usize, j: usize, } impl EntityIterator { fn new(entity: Entity, count_rows: usize, count_cols: usize) -> Self { Self { entity, count_rows, count_cols, i: 0, j: 0, } } } impl Iterator for EntityIterator { type Item = Position; fn next(&mut self) -> Option { if self.count_rows == 0 || self.count_cols == 0 { return None; } match self.entity { Entity::Cell(row, col) => { self.count_cols = 0; self.count_rows = 0; Some(Position::new(row, col)) } Entity::Column(col) => { if self.i >= self.count_rows { return None; } let i = self.i; self.i += 1; Some(Position::new(i, col)) } Entity::Row(row) => { if self.j >= self.count_cols { return None; } let j = self.j; self.j += 1; Some(Position::new(row, j)) } Entity::Global => { if self.j >= self.count_cols { self.j = 0; self.i += 1; if self.i >= self.count_rows { return None; } } let j = self.j; self.j += 1; Some(Position::new(self.i, j)) } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_entity_iter() { assert_eq!( Entity::Global.iter(10, 10).collect::>(), vec![ Position::new(0, 0), Position::new(0, 1), Position::new(0, 2), Position::new(0, 3), Position::new(0, 4), Position::new(0, 5), Position::new(0, 6), Position::new(0, 7), Position::new(0, 8), Position::new(0, 9), Position::new(1, 0), Position::new(1, 1), Position::new(1, 2), Position::new(1, 3), Position::new(1, 4), Position::new(1, 5), Position::new(1, 6), Position::new(1, 7), Position::new(1, 8), Position::new(1, 9), Position::new(2, 0), Position::new(2, 1), Position::new(2, 2), Position::new(2, 3), Position::new(2, 4), Position::new(2, 5), Position::new(2, 6), Position::new(2, 7), Position::new(2, 8), Position::new(2, 9), Position::new(3, 0), Position::new(3, 1), Position::new(3, 2), Position::new(3, 3), Position::new(3, 4), Position::new(3, 5), Position::new(3, 6), Position::new(3, 7), Position::new(3, 8), Position::new(3, 9), Position::new(4, 0), Position::new(4, 1), Position::new(4, 2), Position::new(4, 3), Position::new(4, 4), Position::new(4, 5), Position::new(4, 6), Position::new(4, 7), Position::new(4, 8), Position::new(4, 9), Position::new(5, 0), Position::new(5, 1), Position::new(5, 2), Position::new(5, 3), Position::new(5, 4), Position::new(5, 5), Position::new(5, 6), Position::new(5, 7), Position::new(5, 8), Position::new(5, 9), Position::new(6, 0), Position::new(6, 1), Position::new(6, 2), Position::new(6, 3), Position::new(6, 4), Position::new(6, 5), Position::new(6, 6), Position::new(6, 7), Position::new(6, 8), Position::new(6, 9), Position::new(7, 0), Position::new(7, 1), Position::new(7, 2), Position::new(7, 3), Position::new(7, 4), Position::new(7, 5), Position::new(7, 6), Position::new(7, 7), Position::new(7, 8), Position::new(7, 9), Position::new(8, 0), Position::new(8, 1), Position::new(8, 2), Position::new(8, 3), Position::new(8, 4), Position::new(8, 5), Position::new(8, 6), Position::new(8, 7), Position::new(8, 8), Position::new(8, 9), Position::new(9, 0), Position::new(9, 1), Position::new(9, 2), Position::new(9, 3), Position::new(9, 4), Position::new(9, 5), Position::new(9, 6), Position::new(9, 7), Position::new(9, 8), Position::new(9, 9) ] ); } } papergrid-0.14.0/src/config/formatting.rs000064400000000000000000000012731046102023000164420ustar 00000000000000/// Formatting represent a logic of formatting of a cell text. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Formatting { /// An setting to allow horizontal trim. pub horizontal_trim: bool, /// An setting to allow vertical trim. pub vertical_trim: bool, /// An setting to allow alignment per line. pub allow_lines_alignment: bool, } impl Formatting { /// Creates a new [`Formatting`] structure. pub const fn new(horizontal_trim: bool, vertical_trim: bool, lines_alignment: bool) -> Self { Self { horizontal_trim, vertical_trim, allow_lines_alignment: lines_alignment, } } } papergrid-0.14.0/src/config/horizontal_line.rs000064400000000000000000000027561046102023000174770ustar 00000000000000/// A structure for a custom horizontal line. #[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct HorizontalLine { /// Line character. pub main: Option, /// Line intersection character. pub intersection: Option, /// Left intersection character. pub left: Option, /// Right intersection character. pub right: Option, } impl HorizontalLine { /// Creates a new line. pub const fn new( main: Option, intersection: Option, left: Option, right: Option, ) -> Self { Self { main, intersection, left, right, } } /// Creates a new line. pub const fn full(main: T, intersection: T, left: T, right: T) -> Self { Self::new(Some(main), Some(intersection), Some(left), Some(right)) } /// Creates a new line. pub const fn filled(val: T) -> Self where T: Copy, { Self { main: Some(val), intersection: Some(val), left: Some(val), right: Some(val), } } /// Creates a new line. pub const fn empty() -> Self { Self::new(None, None, None, None) } /// Verifies if the line has any setting set. pub const fn is_empty(&self) -> bool { self.main.is_none() && self.intersection.is_none() && self.left.is_none() && self.right.is_none() } } papergrid-0.14.0/src/config/indent.rs000064400000000000000000000016231046102023000155500ustar 00000000000000/// Indent represent a filled space. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Indent { /// A fill character. pub fill: char, /// A number of repeats of a fill character. pub size: usize, } impl Indent { /// Creates a new Indent structure. pub const fn new(size: usize, fill: char) -> Self { Self { fill, size } } /// Creates a new Indent structure with space (`' '`) as a fill character. pub const fn spaced(size: usize) -> Self { Self { size, fill: ' ' } } /// Creates a new Indent structure with space (`' '`) as a fill character. pub const fn zero() -> Self { Self::new(0, ' ') } /// Verifies whether an indent is set to 0. pub const fn is_empty(&self) -> bool { self.size == 0 } } impl Default for Indent { fn default() -> Self { Self { size: 0, fill: ' ' } } } papergrid-0.14.0/src/config/mod.rs000064400000000000000000000012101046102023000150360ustar 00000000000000//! A module which contains a general settings which might be used in other grid implementations. mod alignment; mod border; mod borders; mod entity; mod formatting; mod horizontal_line; mod indent; mod position; mod sides; mod vertical_line; pub mod compact; #[cfg(feature = "std")] pub mod spanned; pub use alignment::{AlignmentHorizontal, AlignmentVertical}; pub use border::Border; pub use borders::Borders; pub use entity::{Entity, EntityIterator}; pub use formatting::Formatting; pub use horizontal_line::HorizontalLine; pub use indent::Indent; pub use position::{pos, Position}; pub use sides::Sides; pub use vertical_line::VerticalLine; papergrid-0.14.0/src/config/position.rs000064400000000000000000000055611046102023000161400ustar 00000000000000use core::ops::{Add, AddAssign, Sub, SubAssign}; /// Constructs a [`Position`] in a convinient way. /// Basically a short hand for `(row, col).into()` or `Position::new(row, col)`. pub fn pos(row: usize, col: usize) -> Position { Position::new(row, col) } /// Position is a (row, col) position on a Grid. /// /// For example such table has 4 cells. /// Which indexes are (0, 0), (0, 1), (1, 0), (1, 1). /// /// ```text /// ┌───┬───┐ /// │ 0 │ 1 │ /// ├───┌──── /// │ 1 │ 2 │ /// └───┮───┘ /// ``` #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Position { row: usize, col: usize, } impl Position { /// Creates a new [`Position`] object. pub const fn new(row: usize, col: usize) -> Self { Self { row, col } } /// Returns a row value. pub const fn row(&self) -> usize { self.row } /// Returns a column value. pub const fn col(&self) -> usize { self.col } /// A check whether a cell is not beyond the maximum point. /// Meaning it's less then a maximum point. /// So it must be located left and bottom on XY axis. pub const fn is_covered(&self, max: Position) -> bool { self.row < max.row && self.col < max.col } } impl Add for Position { type Output = Position; fn add(self, rhs: Self) -> Self::Output { Self::new(self.row + rhs.row, self.col + rhs.col) } } impl Add<(usize, usize)> for Position { type Output = Position; fn add(self, rhs: (usize, usize)) -> Self::Output { Self::new(self.row + rhs.0, self.col + rhs.1) } } impl Sub for Position { type Output = Position; fn sub(self, rhs: Self) -> Self::Output { Self::new(self.row - rhs.row, self.col - rhs.col) } } impl Sub<(usize, usize)> for Position { type Output = Position; fn sub(self, rhs: (usize, usize)) -> Self::Output { Self::new(self.row - rhs.0, self.col - rhs.1) } } impl AddAssign for Position { fn add_assign(&mut self, rhs: Position) { self.row += rhs.row; self.col += rhs.col; } } impl AddAssign<(usize, usize)> for Position { fn add_assign(&mut self, rhs: (usize, usize)) { self.row += rhs.0; self.col += rhs.1; } } impl SubAssign for Position { fn sub_assign(&mut self, rhs: Position) { self.row -= rhs.row; self.col -= rhs.col; } } impl SubAssign<(usize, usize)> for Position { fn sub_assign(&mut self, rhs: (usize, usize)) { self.row -= rhs.0; self.col -= rhs.1; } } impl From<(usize, usize)> for Position { fn from(value: (usize, usize)) -> Self { Self::new(value.0, value.1) } } impl From for (usize, usize) { fn from(val: Position) -> Self { (val.row(), val.col()) } } papergrid-0.14.0/src/config/sides.rs000064400000000000000000000034221046102023000153750ustar 00000000000000/// A structure which represents 4 box sides. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Sides { /// Top side. pub top: T, /// Bottom side. pub bottom: T, /// Left side. pub left: T, /// Right side. pub right: T, } impl Sides { /// Creates a new object. pub const fn new(left: T, right: T, top: T, bottom: T) -> Self { Self { top, bottom, left, right, } } /// Creates a new object. pub const fn filled(value: T) -> Self where T: Copy, { Self::new(value, value, value, value) } /// Creates a new object. pub fn convert_into(self) -> Sides where T: Into, { Sides::new( self.left.into(), self.right.into(), self.top.into(), self.bottom.into(), ) } /// Converts all sides with a given function. pub fn map(self, f: F) -> Sides where F: Fn(T) -> T1, { Sides::new( (f)(self.left), (f)(self.right), (f)(self.top), (f)(self.bottom), ) } /// Converts all sides with a given function. pub fn fold(self, acc: B, f: F) -> B where F: FnMut(B, T) -> B, { let mut f = f; let mut acc = acc; acc = (f)(acc, self.left); acc = (f)(acc, self.right); acc = (f)(acc, self.top); acc = (f)(acc, self.bottom); acc } } impl Sides> { /// Checkes whether any option was set pub const fn is_empty(&self) -> bool { self.left.is_none() && self.right.is_none() && self.top.is_none() && self.bottom.is_none() } } papergrid-0.14.0/src/config/spanned/borders_config.rs000064400000000000000000000374371046102023000207200ustar 00000000000000use std::collections::{HashMap, HashSet}; use crate::config::{Border, Borders, HorizontalLine, Position, VerticalLine}; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub(crate) struct BordersConfig { global: Option, borders: Borders, cells: BordersMap, horizontals: HashMap>, verticals: HashMap>, layout: BordersLayout, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub(crate) struct BordersMap { vertical: HashMap, horizontal: HashMap, intersection: HashMap, } impl BordersMap { fn is_empty(&self) -> bool { self.vertical.is_empty() && self.horizontal.is_empty() && self.intersection.is_empty() } } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub(crate) struct BordersLayout { left: bool, right: bool, top: bool, bottom: bool, horizontals: HashSet, verticals: HashSet, } impl BordersConfig { pub(crate) fn is_empty(&self) -> bool { self.global.is_none() && self.borders.is_empty() && self.cells.is_empty() && self.horizontals.is_empty() && self.verticals.is_empty() } pub(crate) fn insert_border(&mut self, pos: Position, border: Border) { if let Some(c) = border.top { self.cells.horizontal.insert(pos, c); self.layout.horizontals.insert(pos.row()); } if let Some(c) = border.bottom { self.cells.horizontal.insert(pos + (1, 0), c); self.layout.horizontals.insert(pos.row() + 1); } if let Some(c) = border.left { self.cells.vertical.insert(pos, c); self.layout.verticals.insert(pos.col()); } if let Some(c) = border.right { self.cells.vertical.insert(pos + (0, 1), c); self.layout.verticals.insert(pos.col() + 1); } if let Some(c) = border.left_top_corner { self.cells.intersection.insert(pos, c); self.layout.horizontals.insert(pos.row()); self.layout.verticals.insert(pos.col()); } if let Some(c) = border.right_top_corner { self.cells.intersection.insert(pos + (0, 1), c); self.layout.horizontals.insert(pos.row()); self.layout.verticals.insert(pos.col() + 1); } if let Some(c) = border.left_bottom_corner { self.cells.intersection.insert(pos + (1, 0), c); self.layout.horizontals.insert(pos.row() + 1); self.layout.verticals.insert(pos.col()); } if let Some(c) = border.right_bottom_corner { self.cells.intersection.insert(pos + (1, 1), c); self.layout.horizontals.insert(pos.row() + 1); self.layout.verticals.insert(pos.col() + 1); } } pub(crate) fn remove_border(&mut self, pos: Position, shape: (usize, usize)) { let (count_rows, count_cols) = shape; self.cells.horizontal.remove(&pos); self.cells.horizontal.remove(&(pos + (1, 0))); self.cells.vertical.remove(&pos); self.cells.vertical.remove(&(pos + (0, 1))); self.cells.intersection.remove(&pos); self.cells.intersection.remove(&(pos + (1, 0))); self.cells.intersection.remove(&(pos + (0, 1))); self.cells.intersection.remove(&(pos + (1, 1))); // clean up the layout. if !self.check_is_horizontal_set(pos.row(), count_rows) { self.layout.horizontals.remove(&pos.row()); } if !self.check_is_horizontal_set(pos.row() + 1, count_rows) { self.layout.horizontals.remove(&(pos.row() + 1)); } if !self.check_is_vertical_set(pos.col(), count_cols) { self.layout.verticals.remove(&pos.col()); } if !self.check_is_vertical_set(pos.col() + 1, count_cols) { self.layout.verticals.remove(&(pos.col() + 1)); } } pub(crate) fn get_border(&self, pos: Position, shape: (usize, usize)) -> Border<&T> { Border { top: self.get_horizontal(pos, shape.0), bottom: self.get_horizontal(pos + (1, 0), shape.0), left: self.get_vertical(pos, shape.1), left_top_corner: self.get_intersection(pos, shape), left_bottom_corner: self.get_intersection(pos + (1, 0), shape), right: self.get_vertical(pos + (0, 1), shape.1), right_top_corner: self.get_intersection(pos + (0, 1), shape), right_bottom_corner: self.get_intersection(pos + (1, 1), shape), } } pub(crate) fn insert_horizontal_line(&mut self, row: usize, line: HorizontalLine) { if line.left.is_some() { self.layout.left = true; } // todo: when we delete lines these are still left set; so has_horizontal/vertical return true in some cases; // it shall be fixed, but maybe we can improve the logic as it got a bit complicated. if line.right.is_some() { self.layout.right = true; } self.horizontals.insert(row, line); self.layout.horizontals.insert(row); } pub(crate) fn get_horizontal_line(&self, row: usize) -> Option<&HorizontalLine> { self.horizontals.get(&row) } pub(crate) fn get_horizontal_lines(&self) -> HashMap> where T: Clone, { self.horizontals.clone() } pub(crate) fn remove_horizontal_line(&mut self, row: usize, count_rows: usize) { self.horizontals.remove(&row); self.layout.horizontals.remove(&row); if self.has_horizontal(row, count_rows) { self.layout.horizontals.insert(row); } } pub(crate) fn insert_vertical_line(&mut self, column: usize, line: VerticalLine) { if line.top.is_some() { self.layout.top = true; } if line.bottom.is_some() { self.layout.bottom = true; } self.verticals.insert(column, line); self.layout.verticals.insert(column); } pub(crate) fn get_vertical_line(&self, column: usize) -> Option<&VerticalLine> { self.verticals.get(&column) } pub(crate) fn get_vertical_lines(&self) -> HashMap> where T: Clone, { self.verticals.clone() } pub(crate) fn remove_vertical_line(&mut self, col: usize, count_columns: usize) { self.verticals.remove(&col); self.layout.verticals.remove(&col); if self.has_vertical(col, count_columns) { self.layout.verticals.insert(col); } } pub(crate) fn set_borders(&mut self, borders: Borders) { self.borders = borders; } pub(crate) fn get_borders(&self) -> &Borders { &self.borders } pub(crate) fn get_global(&self) -> Option<&T> { self.global.as_ref() } pub(crate) fn set_global(&mut self, value: T) { self.global = Some(value); } pub(crate) fn get_vertical(&self, pos: Position, count_cols: usize) -> Option<&T> { self.cells .vertical .get(&pos) .or_else(|| self.verticals.get(&pos.col()).and_then(|l| l.main.as_ref())) .or({ if pos.col() == count_cols { self.borders.right.as_ref() } else if pos.col() == 0 { self.borders.left.as_ref() } else { self.borders.vertical.as_ref() } }) .or(self.global.as_ref()) } pub(crate) fn get_horizontal(&self, pos: Position, count_rows: usize) -> Option<&T> { self.cells .horizontal .get(&pos) .or_else(|| { self.horizontals .get(&pos.row()) .and_then(|l| l.main.as_ref()) }) .or({ if pos.row() == 0 { self.borders.top.as_ref() } else if pos.row() == count_rows { self.borders.bottom.as_ref() } else { self.borders.horizontal.as_ref() } }) .or(self.global.as_ref()) } pub(crate) fn get_intersection( &self, pos: Position, (count_rows, count_cols): (usize, usize), ) -> Option<&T> { let use_top = pos.row() == 0; let use_bottom = pos.row() == count_rows; let use_left = pos.col() == 0; let use_right = pos.col() == count_cols; let intersection = self.cells.intersection.get(&pos); if intersection.is_some() { return intersection; } let intersection = self.horizontals.get(&pos.row()).and_then(|l| { if use_left && l.left.is_some() { l.left.as_ref() } else if use_right && l.right.is_some() { l.right.as_ref() } else if !use_right && !use_left && l.intersection.is_some() { l.intersection.as_ref() } else { None } }); if intersection.is_some() { return intersection; } let intersection = self.verticals.get(&pos.col()).and_then(|l| { if use_top && l.top.is_some() { l.top.as_ref() } else if use_bottom && l.bottom.is_some() { l.bottom.as_ref() } else if !use_top && !use_bottom && l.intersection.is_some() { l.intersection.as_ref() } else { None } }); if intersection.is_some() { return intersection; } let intersection = { if use_top && use_left { self.borders.top_left.as_ref() } else if use_top && use_right { self.borders.top_right.as_ref() } else if use_bottom && use_left { self.borders.bottom_left.as_ref() } else if use_bottom && use_right { self.borders.bottom_right.as_ref() } else if use_top { self.borders.top_intersection.as_ref() } else if use_bottom { self.borders.bottom_intersection.as_ref() } else if use_left { self.borders.left_intersection.as_ref() } else if use_right { self.borders.right_intersection.as_ref() } else { self.borders.intersection.as_ref() } }; if intersection.is_some() { return intersection; } self.global.as_ref() } pub(crate) fn has_horizontal(&self, row: usize, count_rows: usize) -> bool { self.global.is_some() || (row == 0 && self.borders.has_top()) || (row == count_rows && self.borders.has_bottom()) || (row > 0 && row < count_rows && self.borders.has_horizontal()) || self.is_horizontal_set(row, count_rows) } pub(crate) fn has_vertical(&self, col: usize, count_cols: usize) -> bool { self.global.is_some() || (col == 0 && self.borders.has_left()) || (col == count_cols && self.borders.has_right()) || (col > 0 && col < count_cols && self.borders.has_vertical()) || self.is_vertical_set(col, count_cols) } fn is_horizontal_set(&self, row: usize, count_rows: usize) -> bool { (row == 0 && self.layout.top) || (row == count_rows && self.layout.bottom) || self.layout.horizontals.contains(&row) } fn is_vertical_set(&self, col: usize, count_cols: usize) -> bool { (col == 0 && self.layout.left) || (col == count_cols && self.layout.right) || self.layout.verticals.contains(&col) } fn check_is_horizontal_set(&self, row: usize, count_rows: usize) -> bool { (row == 0 && self.layout.top) || (row == count_rows && self.layout.bottom) || self.cells.horizontal.keys().any(|&p| p.row() == row) || self.cells.intersection.keys().any(|&p| p.row() == row) } fn check_is_vertical_set(&self, col: usize, count_cols: usize) -> bool { (col == 0 && self.layout.left) || (col == count_cols && self.layout.right) || self.cells.vertical.keys().any(|&p| p.col() == col) || self.cells.intersection.keys().any(|&p| p.col() == col) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_insert_border() { let mut borders = BordersConfig::::default(); borders.insert_border((0, 0).into(), Border::filled('x')); assert_eq!( borders.get_border((0, 0).into(), (10, 10)), Border::filled(&'x') ); assert_eq!( borders.get_border((0, 0).into(), (0, 0)), Border::filled(&'x') ); assert!(borders.is_horizontal_set(0, 10)); assert!(borders.is_horizontal_set(1, 10)); assert!(!borders.is_horizontal_set(2, 10)); assert!(borders.is_vertical_set(0, 10)); assert!(borders.is_vertical_set(1, 10)); assert!(!borders.is_vertical_set(2, 10)); assert!(borders.is_horizontal_set(0, 0)); assert!(borders.is_horizontal_set(1, 0)); assert!(!borders.is_horizontal_set(2, 0)); assert!(borders.is_vertical_set(0, 0)); assert!(borders.is_vertical_set(1, 0)); assert!(!borders.is_vertical_set(2, 0)); } #[test] fn test_insert_border_override() { let mut borders = BordersConfig::::default(); borders.insert_border((0, 0).into(), Border::filled('x')); borders.insert_border((1, 0).into(), Border::filled('y')); borders.insert_border((0, 1).into(), Border::filled('w')); borders.insert_border((1, 1).into(), Border::filled('q')); assert_eq!( borders.get_border((0, 0).into(), (10, 10)).copied(), Border::full('x', 'y', 'x', 'w', 'x', 'w', 'y', 'q') ); assert_eq!( borders.get_border((0, 1).into(), (10, 10)).copied(), Border::full('w', 'q', 'w', 'w', 'w', 'w', 'q', 'q') ); assert_eq!( borders.get_border((1, 0).into(), (10, 10)).copied(), Border::full('y', 'y', 'y', 'q', 'y', 'q', 'y', 'q') ); assert_eq!( borders.get_border((1, 1).into(), (10, 10)).copied(), Border::filled('q') ); assert!(borders.is_horizontal_set(0, 10)); assert!(borders.is_horizontal_set(1, 10)); assert!(borders.is_horizontal_set(2, 10)); assert!(!borders.is_horizontal_set(3, 10)); assert!(borders.is_vertical_set(0, 10)); assert!(borders.is_vertical_set(1, 10)); assert!(borders.is_vertical_set(2, 10)); assert!(!borders.is_vertical_set(3, 10)); } #[test] fn test_set_global() { let mut borders = BordersConfig::::default(); borders.insert_border((0, 0).into(), Border::filled('x')); borders.set_global('l'); assert_eq!( borders.get_border((0, 0).into(), (10, 10)), Border::filled(&'x') ); assert_eq!( borders.get_border((2, 0).into(), (10, 10)), Border::filled(&'l') ); assert!(borders.is_horizontal_set(0, 10)); assert!(borders.is_horizontal_set(1, 10)); assert!(!borders.is_horizontal_set(2, 10)); assert!(borders.is_vertical_set(0, 10)); assert!(borders.is_vertical_set(1, 10)); assert!(!borders.is_vertical_set(2, 10)); assert!(borders.is_horizontal_set(0, 0)); assert!(borders.is_horizontal_set(1, 0)); assert!(!borders.is_horizontal_set(2, 0)); assert!(borders.is_vertical_set(0, 0)); assert!(borders.is_vertical_set(1, 0)); assert!(!borders.is_vertical_set(2, 0)); } } papergrid-0.14.0/src/config/spanned/entity_map.rs000064400000000000000000000100151046102023000200630ustar 00000000000000use std::collections::HashMap; use fnv::FnvHashMap; use crate::config::{Entity, Position}; /// A structure to keep information for [`Entity`] as a key. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct EntityMap { // we have a global type to allocate in on stack. // because most of the time no changes are made to the [`EntityMap`]. global: T, columns: FnvHashMap, rows: FnvHashMap, cells: FnvHashMap, } impl EntityMap { /// Creates an empty [`EntityMap`]. pub fn new(global: T) -> Self { Self { global, rows: FnvHashMap::default(), columns: FnvHashMap::default(), cells: FnvHashMap::default(), } } /// Verifies whether anything was set beside a global entry. pub fn is_empty(&self) -> bool { self.columns.is_empty() && self.rows.is_empty() && self.cells.is_empty() } /// Get a value for an [`Entity`]. pub fn get(&self, pos: Position) -> &T { // todo: optimize; // // Cause we can change rows/columns/cells separately we need to check them separately. // But we often doing this checks in `Grid::fmt` and I believe if we could optimize it it could be beneficial. // // Haven't found a solution for that yet. // // I was wondering if there is a hash function like. // Apparently it doesn't make sense cause we will reset columns/rows on cell insert which is not what we want. // // ``` // hash(column, row) == hash(column) == hash(row) // ``` // // ref: https://opendsa-server.cs.vt.edu/ODSA/Books/Everything/html/Sparse.html // ref: https://users.rust-lang.org/t/make-hash-return-same-value-whather-the-order-of-element-of-a-tuple/69932/13 self.cells .get(&pos) .or_else(|| self.columns.get(&pos.col())) .or_else(|| self.rows.get(&pos.row())) .unwrap_or(&self.global) } /// Removes a value for an [`Entity`]. pub fn remove(&mut self, entity: Entity) { match entity { Entity::Global => { self.cells.clear(); self.rows.clear(); self.columns.clear(); } Entity::Column(col) => self.cells.retain(|pos, _| pos.col() != col), Entity::Row(row) => self.cells.retain(|pos, _| pos.row() != row), Entity::Cell(row, col) => { self.cells.remove(&Position::new(row, col)); } } } } impl EntityMap { /// Set a value for an [`Entity`]. pub fn insert(&mut self, entity: Entity, value: T) { match entity { Entity::Column(col) => { for &row in self.rows.keys() { self.cells.insert(Position::new(row, col), value.clone()); } self.columns.insert(col, value); } Entity::Row(row) => { for &col in self.columns.keys() { self.cells.insert(Position::new(row, col), value.clone()); } self.rows.insert(row, value); } Entity::Cell(row, col) => { self.cells.insert(Position::new(row, col), value); } Entity::Global => { self.remove(Entity::Global); self.global = value } } } } impl From> for HashMap { fn from(value: EntityMap) -> Self { let mut m = HashMap::new(); m.insert(Entity::Global, value.global); for (pos, value) in value.cells { m.insert(Entity::from(pos), value); } for (row, value) in value.rows { m.insert(Entity::Row(row), value); } for (col, value) in value.columns { m.insert(Entity::Column(col), value); } m } } impl AsRef for EntityMap { fn as_ref(&self) -> &T { &self.global } } papergrid-0.14.0/src/config/spanned/mod.rs000064400000000000000000000753011046102023000165020ustar 00000000000000//! A module which contains configuration options for a [`Grid`]. //! //! [`Grid`]: crate::grid::iterable::Grid mod borders_config; mod entity_map; mod offset; use std::collections::HashMap; use crate::ansi::{ANSIBuf, ANSIStr}; use crate::config::compact::CompactConfig; use crate::config::{ AlignmentHorizontal, AlignmentVertical, Border, Borders, Entity, Indent, Position, Sides, }; use borders_config::BordersConfig; pub use self::{entity_map::EntityMap, offset::Offset}; use super::Formatting; /// HorizontalLine represents a horizontal border line. type HorizontalLine = super::HorizontalLine; /// VerticalLine represents a vertical border line. type VerticalLine = super::VerticalLine; /// This structure represents a settings of a grid. /// /// grid: crate::Grid. #[derive(Debug, PartialEq, Eq, Clone)] pub struct SpannedConfig { margin: Sides, padding: EntityMap>, padding_color: EntityMap>>, alignment_h: EntityMap, alignment_v: EntityMap, formatting_trim_h: EntityMap, formatting_trim_v: EntityMap, formatting_line_alignment: EntityMap, span_columns: HashMap, span_rows: HashMap, borders: BordersConfig, borders_colors: BordersConfig, borders_missing_char: char, horizontal_chars: HashMap>, horizontal_colors: HashMap>, // squash a map to be HashMap<(Pos, Offset), char> vertical_chars: HashMap>, vertical_colors: HashMap>, justification: EntityMap, justification_color: EntityMap>, } impl Default for SpannedConfig { fn default() -> Self { Self { margin: Sides::default(), padding: EntityMap::default(), padding_color: EntityMap::default(), formatting_trim_h: EntityMap::default(), formatting_trim_v: EntityMap::default(), formatting_line_alignment: EntityMap::default(), alignment_h: EntityMap::new(AlignmentHorizontal::Left), alignment_v: EntityMap::new(AlignmentVertical::Top), span_columns: HashMap::default(), span_rows: HashMap::default(), borders: BordersConfig::default(), borders_colors: BordersConfig::default(), borders_missing_char: ' ', horizontal_chars: HashMap::default(), horizontal_colors: HashMap::default(), vertical_chars: HashMap::default(), vertical_colors: HashMap::default(), justification: EntityMap::new(' '), justification_color: EntityMap::default(), } } } impl SpannedConfig { /// Set a margin of a grid. pub fn set_margin(&mut self, margin: Sides) { self.margin.left.indent = margin.left; self.margin.right.indent = margin.right; self.margin.top.indent = margin.top; self.margin.bottom.indent = margin.bottom; } /// Set a color of margin of a grid. pub fn set_margin_color(&mut self, margin: Sides>) { self.margin.left.color = margin.left; self.margin.right.color = margin.right; self.margin.top.color = margin.top; self.margin.bottom.color = margin.bottom; } /// Set an offset of margin of a grid. pub fn set_margin_offset(&mut self, margin: Sides) { self.margin.left.offset = margin.left; self.margin.right.offset = margin.right; self.margin.top.offset = margin.top; self.margin.bottom.offset = margin.bottom; } /// Returns a margin value currently set. pub fn get_margin(&self) -> Sides { Sides::new( self.margin.left.indent, self.margin.right.indent, self.margin.top.indent, self.margin.bottom.indent, ) } /// Returns a margin color value currently set. pub fn get_margin_color(&self) -> Sides> { Sides::new( self.margin.left.color.clone(), self.margin.right.color.clone(), self.margin.top.color.clone(), self.margin.bottom.color.clone(), ) } /// Returns a margin offset value currently set. pub fn get_margin_offset(&self) -> Sides { Sides::new( self.margin.left.offset, self.margin.right.offset, self.margin.top.offset, self.margin.bottom.offset, ) } /// Removes border changes. pub fn remove_borders(&mut self) { self.borders = BordersConfig::default(); } /// Removes border changes. pub fn remove_borders_colors(&mut self) { self.borders_colors = BordersConfig::default(); } /// Removes border changes. pub fn remove_color_line_horizontal(&mut self) { self.horizontal_colors.clear(); } /// Removes border changes. pub fn remove_color_line_vertical(&mut self) { self.vertical_colors.clear(); } /// Removes border changes. pub fn remove_horizontal_chars(&mut self) { self.horizontal_chars.clear(); } /// Removes border changes. pub fn remove_vertical_chars(&mut self) { self.vertical_chars.clear(); } /// Set the [`Borders`] value as correct one. pub fn set_borders(&mut self, borders: Borders) { self.borders.set_borders(borders); } /// Gets a global border value if set. pub fn get_border_default(&self) -> Option<&char> { self.borders.get_global() } /// Set the all [`Borders`] values to a char. pub fn set_border_default(&mut self, c: char) { self.borders.set_global(c); } /// Returns a current [`Borders`] structure. pub fn get_borders(&self) -> &Borders { self.borders.get_borders() } /// Set the border line by row index. /// /// Row `0` means the top row. /// Row `grid.count_rows()` means the bottom row. pub fn insert_horizontal_line(&mut self, line: usize, val: HorizontalLine) { self.borders.insert_horizontal_line(line, val); } /// Sets off the border line by row index if any were set /// /// Row `0` means the top row. /// Row `grid.count_rows()` means the bottom row. pub fn remove_horizontal_line(&mut self, line: usize, count_rows: usize) { self.borders.remove_horizontal_line(line, count_rows); } /// Gets a overridden vertical line. /// /// Row `0` means the left row. /// Row `grid.count_columns()` means the right most row. pub fn get_vertical_line(&self, line: usize) -> Option<&VerticalLine> { self.borders.get_vertical_line(line) } /// Gets all overridden vertical lines. /// /// Row `0` means the top row. /// Row `grid.count_rows()` means the bottom row. pub fn get_vertical_lines(&self) -> HashMap { self.borders.get_vertical_lines() } /// Set the border line by column index. /// /// Row `0` means the left row. /// Row `grid.count_columns()` means the right most row. pub fn insert_vertical_line(&mut self, line: usize, val: VerticalLine) { self.borders.insert_vertical_line(line, val); } /// Sets off the border line by column index if any were set /// /// Row `0` means the left row. /// Row `grid.count_columns()` means the right most row. pub fn remove_vertical_line(&mut self, line: usize, count_columns: usize) { self.borders.remove_vertical_line(line, count_columns); } /// Gets a overridden line. /// /// Row `0` means the top row. /// Row `grid.count_rows()` means the bottom row. pub fn get_horizontal_line(&self, line: usize) -> Option<&HorizontalLine> { self.borders.get_horizontal_line(line) } /// Gets all overridden lines. /// /// Row `0` means the top row. /// Row `grid.count_rows()` means the bottom row. pub fn get_horizontal_lines(&self) -> HashMap { self.borders.get_horizontal_lines() } /// Override a character on a horizontal line. /// /// If borders are not set the char won't be used. /// /// It takes not cell position but line as row and column of a cell; /// So its range is line <= count_rows && col < count_columns. pub fn set_horizontal_char(&mut self, pos: Position, c: char, offset: Offset) { let chars = self .horizontal_chars .entry(pos) .or_insert_with(|| HashMap::with_capacity(1)); chars.insert(offset, c); } /// Get a list of overridden chars in a horizontal border. /// /// It takes not cell position but line as row and column of a cell; /// So its range is line <= count_rows && col < count_columns. pub fn lookup_horizontal_char(&self, pos: Position, offset: usize, end: usize) -> Option { self.horizontal_chars .get(&pos) .and_then(|chars| { chars.get(&Offset::Begin(offset)).or_else(|| { if end > offset { if end == 0 { chars.get(&Offset::End(0)) } else { chars.get(&Offset::End(end - offset - 1)) } } else { None } }) }) .copied() } /// Checks if there any char in a horizontal border being overridden. /// /// It takes not cell position but line as row and column of a cell; /// So its range is line <= count_rows && col < count_columns. pub fn is_overridden_horizontal(&self, pos: Position) -> bool { self.horizontal_chars.contains_key(&pos) } /// Removes a list of overridden chars in a horizontal border. /// /// It takes not cell position but line as row and column of a cell; /// So its range is line <= count_rows && col < count_columns. pub fn remove_overridden_horizontal(&mut self, pos: Position) { self.horizontal_chars.remove(&pos); } /// Override a vertical split line. /// /// If borders are not set the char won't be used. /// /// It takes not cell position but cell row and column of a line; /// So its range is row < count_rows && col <= count_columns. pub fn set_vertical_char(&mut self, pos: Position, c: char, offset: Offset) { let chars = self .vertical_chars .entry(pos) .or_insert_with(|| HashMap::with_capacity(1)); chars.insert(offset, c); } /// Get a list of overridden chars in a horizontal border. /// /// It takes not cell position but cell row and column of a line; /// So its range is row < count_rows && col <= count_columns. pub fn lookup_vertical_char(&self, pos: Position, offset: usize, end: usize) -> Option { self.vertical_chars .get(&pos) .and_then(|chars| { chars.get(&Offset::Begin(offset)).or_else(|| { if end > offset { if end == 0 { chars.get(&Offset::End(0)) } else { chars.get(&Offset::End(end - offset - 1)) } } else { None } }) }) .copied() } /// Checks if there any char in a horizontal border being overridden. /// /// It takes not cell position but cell row and column of a line; /// So its range is row < count_rows && col <= count_columns. pub fn is_overridden_vertical(&self, pos: Position) -> bool { self.vertical_chars.contains_key(&pos) } /// Removes a list of overridden chars in a horizontal border. /// /// It takes not cell position but cell row and column of a line; /// So its range is row < count_rows && col <= count_columns. pub fn remove_overridden_vertical(&mut self, pos: Position) { self.vertical_chars.remove(&pos); } /// Override a character color on a horizontal line. pub fn set_horizontal_color(&mut self, pos: Position, c: ANSIBuf, offset: Offset) { let chars = self .horizontal_colors .entry(pos) .or_insert_with(|| HashMap::with_capacity(1)); chars.insert(offset, c); } /// Get a overridden color in a horizontal border. pub fn lookup_horizontal_color( &self, pos: Position, offset: usize, end: usize, ) -> Option<&ANSIBuf> { self.horizontal_colors.get(&pos).and_then(|chars| { chars.get(&Offset::Begin(offset)).or_else(|| { if end > offset { if end == 0 { chars.get(&Offset::End(0)) } else { chars.get(&Offset::End(end - offset - 1)) } } else { None } }) }) } /// Override a character color on a vertical line. pub fn set_vertical_color(&mut self, pos: Position, c: ANSIBuf, offset: Offset) { let chars = self .vertical_colors .entry(pos) .or_insert_with(|| HashMap::with_capacity(1)); chars.insert(offset, c); } /// Get a overridden color in a vertical border. pub fn lookup_vertical_color( &self, pos: Position, offset: usize, end: usize, ) -> Option<&ANSIBuf> { self.vertical_colors.get(&pos).and_then(|chars| { chars.get(&Offset::Begin(offset)).or_else(|| { if end > offset { if end == 0 { chars.get(&Offset::End(0)) } else { chars.get(&Offset::End(end - offset - 1)) } } else { None } }) }) } /// Set a padding to a given cells. pub fn set_padding(&mut self, entity: Entity, padding: Sides) { self.padding.insert(entity, padding); } /// Set a padding to a given cells. pub fn set_padding_color(&mut self, entity: Entity, padding: Sides>) { self.padding_color.insert(entity, padding); } /// Get a padding for a given cell by [Position]. pub fn get_padding(&self, pos: Position) -> Sides { *self.padding.get(pos) } /// Get a padding color for a given cell by [Position]. pub fn get_padding_color(&self, pos: Position) -> Sides> { self.padding_color.get(pos).clone() } /// Set a formatting to a given cells. pub fn set_trim_horizontal(&mut self, entity: Entity, on: bool) { self.formatting_trim_h.insert(entity, on); } /// Get a formatting settings for a given cell by [Position]. pub fn get_trim_horizonal(&self, pos: Position) -> bool { *self.formatting_trim_h.get(pos) } /// Set a formatting to a given cells. pub fn set_trim_vertical(&mut self, entity: Entity, on: bool) { self.formatting_trim_v.insert(entity, on); } /// Get a formatting settings for a given cell by [Position]. pub fn get_trim_vertical(&self, pos: Position) -> bool { *self.formatting_trim_v.get(pos) } /// Set a formatting to a given cells. pub fn set_line_alignment(&mut self, entity: Entity, on: bool) { self.formatting_line_alignment.insert(entity, on); } /// Get a formatting settings for a given cell by [Position]. pub fn get_line_alignment(&self, pos: Position) -> bool { *self.formatting_line_alignment.get(pos) } /// Get a formatting settings for a given cell by [Position]. pub fn get_formatting(&self, pos: Position) -> Formatting { Formatting::new( *self.formatting_trim_h.get(pos), *self.formatting_trim_v.get(pos), *self.formatting_line_alignment.get(pos), ) } /// Set a vertical alignment to a given cells. pub fn set_alignment_vertical(&mut self, entity: Entity, alignment: AlignmentVertical) { self.alignment_v.insert(entity, alignment); } /// Get a vertical alignment for a given cell by [Position]. pub fn get_alignment_vertical(&self, pos: Position) -> &AlignmentVertical { self.alignment_v.get(pos) } /// Set a horizontal alignment to a given cells. pub fn set_alignment_horizontal(&mut self, entity: Entity, alignment: AlignmentHorizontal) { self.alignment_h.insert(entity, alignment); } /// Get a horizontal alignment for a given cell by [Position]. pub fn get_alignment_horizontal(&self, pos: Position) -> &AlignmentHorizontal { self.alignment_h.get(pos) } /// Set border set a border value to all cells in [`Entity`]. pub fn set_border(&mut self, pos: Position, border: Border) { self.borders.insert_border(pos, border); } /// Returns a border of a cell. pub fn get_border(&self, pos: Position, shape: (usize, usize)) -> Border { self.borders.get_border(pos, shape).copied() } /// Returns a border color of a cell. pub fn get_border_color(&self, pos: Position, shape: (usize, usize)) -> Border<&ANSIBuf> { self.borders_colors.get_border(pos, shape) } /// Set a character which will be used in case any misconfiguration of borders. /// /// It will be usde for example when you set a left char for border frame and top but didn't set a top left corner. pub fn set_borders_missing(&mut self, c: char) { self.borders_missing_char = c; } /// Get a character which will be used in case any misconfiguration of borders. pub fn get_borders_missing(&self) -> char { self.borders_missing_char } /// Gets a color of all borders on the grid. pub fn get_border_color_default(&self) -> Option<&ANSIBuf> { self.borders_colors.get_global() } /// Sets a color of all borders on the grid. pub fn set_border_color_default(&mut self, clr: ANSIBuf) { self.borders_colors = BordersConfig::default(); self.borders_colors.set_global(clr); } /// Gets colors of a borders carcass on the grid. pub fn get_color_borders(&self) -> &Borders { self.borders_colors.get_borders() } /// Sets colors of border carcass on the grid. pub fn set_borders_color(&mut self, clrs: Borders) { self.borders_colors.set_borders(clrs); } /// Sets a color of border of a cell on the grid. pub fn set_border_color(&mut self, pos: Position, border: Border) { self.borders_colors.insert_border(pos, border) } /// Sets off all borders possible on the [`Entity`]. /// /// It doesn't changes globally set borders through [`SpannedConfig::set_borders`]. // // todo: would be great to remove a shape pub fn remove_border(&mut self, pos: Position, shape: (usize, usize)) { self.borders.remove_border(pos, shape); } /// Gets a color of border of a cell on the grid. // // todo: would be great to remove a shape pub fn remove_border_color(&mut self, pos: Position, shape: (usize, usize)) { self.borders_colors.remove_border(pos, shape); } /// Get a justification which will be used while expanding cells width/height. pub fn get_justification(&self, pos: Position) -> char { *self.justification.get(pos) } /// Get a justification color which will be used while expanding cells width/height. /// /// `None` means no color. pub fn get_justification_color(&self, pos: Position) -> Option<&ANSIBuf> { self.justification_color.get(pos).as_ref() } /// Set a justification which will be used while expanding cells width/height. pub fn set_justification(&mut self, entity: Entity, c: char) { self.justification.insert(entity, c); } /// Set a justification color which will be used while expanding cells width/height. /// /// `None` removes it. pub fn set_justification_color(&mut self, entity: Entity, color: Option) { self.justification_color.insert(entity, color); } /// Get a span value of the cell, if any is set. pub fn get_column_spans(&self) -> HashMap { self.span_columns.clone() } /// Get a span value of the cell, if any is set. pub fn get_row_spans(&self) -> HashMap { self.span_rows.clone() } /// Get a span value of the cell, if any is set. pub fn get_column_span(&self, pos: Position) -> Option { self.span_columns.get(&pos).copied() } /// Get a span value of the cell, if any is set. pub fn get_row_span(&self, pos: Position) -> Option { self.span_rows.get(&pos).copied() } /// Removes column spans. pub fn remove_column_spans(&mut self) { self.span_columns.clear() } /// Removes row spans. pub fn remove_row_spans(&mut self) { self.span_rows.clear() } /// Set a column span to a given cells. /// /// BEWARE /// /// IT'S CALLER RESPONSIBILITY TO MAKE SURE /// THAT THERE NO INTERSECTIONS IN PLACE AND THE SPAN VALUE IS CORRECT pub fn set_column_span(&mut self, pos: Position, span: usize) { set_cell_column_span(self, pos, span); } /// Verifies if there's any spans set. pub fn has_column_spans(&self) -> bool { !self.span_columns.is_empty() } /// Set a column span to a given cells. /// /// BEWARE /// /// IT'S CALLER RESPONSIBILITY TO MAKE SURE /// THAT THERE NO INTERSECTIONS IN PLACE AND THE SPAN VALUE IS CORRECT pub fn set_row_span(&mut self, pos: Position, span: usize) { set_cell_row_span(self, pos, span); } /// Verifies if there's any spans set. pub fn has_row_spans(&self) -> bool { !self.span_rows.is_empty() } /// Verifies if there's any colors set for a borders. pub fn has_border_colors(&self) -> bool { !self.borders_colors.is_empty() } /// Verifies if there's any colors set for a borders. pub fn has_offset_chars(&self) -> bool { !self.horizontal_chars.is_empty() || !self.vertical_chars.is_empty() } /// Verifies if there's any colors set for a borders. pub fn has_justification(&self) -> bool { !self.justification.is_empty() || !self.justification_color.is_empty() || self.justification_color.as_ref().is_some() } /// Verifies if there's any custom padding set. pub fn has_padding(&self) -> bool { !self.padding.is_empty() } /// Verifies if there's any custom padding set. pub fn has_padding_color(&self) -> bool { if !self.padding_color.is_empty() { let map = HashMap::from(self.padding_color.clone()); for (entity, value) in map { if matches!(entity, Entity::Global) { continue; } if !value.is_empty() { return true; } } } !self.padding_color.as_ref().is_empty() } /// Verifies if there's any custom formatting set. pub fn has_formatting(&self) -> bool { !self.formatting_trim_h.is_empty() || !self.formatting_trim_v.is_empty() || !self.formatting_line_alignment.is_empty() } /// Verifies if there's any custom alignment vertical set. pub fn has_alignment_vertical(&self) -> bool { !self.alignment_v.is_empty() } /// Verifies if there's any custom alignment horizontal set. pub fn has_alignment_horizontal(&self) -> bool { !self.alignment_h.is_empty() } /// Gets an intersection character which would be rendered on the grid. /// /// grid: crate::Grid pub fn get_intersection(&self, pos: Position, shape: (usize, usize)) -> Option { let c = self.borders.get_intersection(pos, shape); if let Some(c) = c { return Some(*c); } if self.has_horizontal(pos.row(), shape.0) && self.has_vertical(pos.col(), shape.1) { return Some(self.get_borders_missing()); } None } /// Gets a horizontal character which would be rendered on the grid. /// /// grid: crate::Grid pub fn get_horizontal(&self, pos: Position, count_rows: usize) -> Option { let c = self.borders.get_horizontal(pos, count_rows); if let Some(c) = c { return Some(*c); } if self.has_horizontal(pos.row(), count_rows) { return Some(self.get_borders_missing()); } None } /// Gets a vertical character which would be rendered on the grid. /// /// grid: crate::Grid pub fn get_vertical(&self, pos: Position, count_columns: usize) -> Option { if let Some(c) = self.borders.get_vertical(pos, count_columns) { return Some(*c); } if self.has_vertical(pos.col(), count_columns) { return Some(self.get_borders_missing()); } None } /// Gets a color of a cell horizontal. pub fn get_horizontal_color(&self, pos: Position, count_rows: usize) -> Option<&ANSIBuf> { self.borders_colors.get_horizontal(pos, count_rows) } /// Gets a color of a cell vertical. pub fn get_vertical_color(&self, pos: Position, count_columns: usize) -> Option<&ANSIBuf> { self.borders_colors.get_vertical(pos, count_columns) } /// Gets a color of a cell vertical. pub fn get_intersection_color(&self, pos: Position, shape: (usize, usize)) -> Option<&ANSIBuf> { self.borders_colors.get_intersection(pos, shape) } /// Checks if grid would have a horizontal border with the current configuration. /// /// grid: crate::Grid pub fn has_horizontal(&self, row: usize, count_rows: usize) -> bool { self.borders.has_horizontal(row, count_rows) } /// Checks if grid would have a vertical border with the current configuration. /// /// grid: crate::Grid pub fn has_vertical(&self, col: usize, count_columns: usize) -> bool { self.borders.has_vertical(col, count_columns) } /// Calculates an amount of horizontal lines would present on the grid. /// /// grid: crate::Grid pub fn count_horizontal(&self, count_rows: usize) -> usize { (0..=count_rows) .filter(|&row| self.has_horizontal(row, count_rows)) .count() } /// Calculates an amount of vertical lines would present on the grid. /// /// grid: crate::Grid pub fn count_vertical(&self, count_columns: usize) -> usize { (0..=count_columns) .filter(|&col| self.has_vertical(col, count_columns)) .count() } /// The function returns whether the cells will be rendered or it will be hidden because of a span. pub fn is_cell_visible(&self, pos: Position) -> bool { !(self.is_cell_covered_by_column_span(pos) || self.is_cell_covered_by_row_span(pos) || self.is_cell_covered_by_both_spans(pos)) } /// The function checks if a cell is hidden because of a row span. pub fn is_cell_covered_by_row_span(&self, pos: Position) -> bool { is_cell_covered_by_row_span(self, pos) } /// The function checks if a cell is hidden because of a column span. pub fn is_cell_covered_by_column_span(&self, pos: Position) -> bool { is_cell_covered_by_column_span(self, pos) } /// The function checks if a cell is hidden indirectly because of a row and column span combination. pub fn is_cell_covered_by_both_spans(&self, pos: Position) -> bool { is_cell_covered_by_both_spans(self, pos) } } impl From for SpannedConfig { fn from(compact: CompactConfig) -> Self { use Entity::Global; let mut cfg = Self::default(); cfg.set_padding(Global, *compact.get_padding()); cfg.set_padding_color(Global, to_ansi_color(*compact.get_padding_color())); cfg.set_margin(*compact.get_margin()); cfg.set_margin_color(to_ansi_color(*compact.get_margin_color())); cfg.set_alignment_horizontal(Global, compact.get_alignment_horizontal()); cfg.set_borders(*compact.get_borders()); cfg.set_borders_color(compact.get_borders_color().convert_into()); cfg } } fn to_ansi_color(b: Sides>) -> Sides> { Sides::new( Some(b.left.into()), Some(b.right.into()), Some(b.top.into()), Some(b.bottom.into()), ) } fn set_cell_row_span(cfg: &mut SpannedConfig, pos: Position, span: usize) { // such spans aren't supported if span == 0 { return; } // It's a default span so we can do nothing. // but we check if it's an override of a span. if span == 1 { cfg.span_rows.remove(&pos); return; } cfg.span_rows.insert(pos, span); } fn set_cell_column_span(cfg: &mut SpannedConfig, pos: Position, span: usize) { // such spans aren't supported if span == 0 { return; } // It's a default span so we can do nothing. // but we check if it's an override of a span. if span == 1 { cfg.span_columns.remove(&pos); return; } cfg.span_columns.insert(pos, span); } fn is_cell_covered_by_column_span(cfg: &SpannedConfig, pos: Position) -> bool { cfg.span_columns .iter() .any(|(p, span)| p.row() == pos.row() && pos.col() > p.col() && pos.col() < p.col() + span) } fn is_cell_covered_by_row_span(cfg: &SpannedConfig, pos: Position) -> bool { cfg.span_rows .iter() .any(|(p, span)| p.col() == pos.col() && pos.row() > p.row() && pos.row() < p.row() + span) } fn is_cell_covered_by_both_spans(cfg: &SpannedConfig, pos: Position) -> bool { if !cfg.has_column_spans() || !cfg.has_row_spans() { return false; } cfg.span_rows.iter().any(|(p1, row_span)| { cfg.span_columns .iter() .filter(|(p2, _)| &p1 == p2) .any(|(_, col_span)| { pos.row() > p1.row() && pos.row() < p1.row() + row_span && pos.col() > p1.col() && pos.col() < p1.col() + col_span }) }) } /// A colorefull margin indent. #[derive(Debug, Clone, PartialEq, Eq)] struct MarginIndent { /// An indent value. indent: Indent, /// An offset value. offset: Offset, /// An color value. color: Option, } impl Default for MarginIndent { fn default() -> Self { Self { indent: Indent::default(), offset: Offset::Begin(0), color: None, } } } papergrid-0.14.0/src/config/spanned/offset.rs000064400000000000000000000003561046102023000172070ustar 00000000000000/// The structure represents an offset in a text. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum Offset { /// An offset from the start. Begin(usize), /// An offset from the end. End(usize), } papergrid-0.14.0/src/config/vertical_line.rs000064400000000000000000000027411046102023000171110ustar 00000000000000/// A structure for a vertical line. #[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct VerticalLine { /// Line character. pub main: Option, /// Line intersection character. pub intersection: Option, /// Left intersection character. pub top: Option, /// Right intersection character. pub bottom: Option, } impl VerticalLine { /// Creates a new line. pub const fn new( main: Option, intersection: Option, top: Option, bottom: Option, ) -> Self { Self { main, intersection, top, bottom, } } /// Creates a new line. pub const fn full(main: T, intersection: T, left: T, right: T) -> Self { Self::new(Some(main), Some(intersection), Some(left), Some(right)) } /// Creates a new line. pub const fn filled(val: T) -> Self where T: Copy, { Self { main: Some(val), intersection: Some(val), top: Some(val), bottom: Some(val), } } /// Creates a new line. pub const fn empty() -> Self { Self::new(None, None, None, None) } /// Verifies if the line has any setting set. pub const fn is_empty(&self) -> bool { self.main.is_none() && self.intersection.is_none() && self.top.is_none() && self.bottom.is_none() } } papergrid-0.14.0/src/dimension/compact.rs000064400000000000000000000074251046102023000164430ustar 00000000000000//! The module contains a [`CompactGridDimension`] for [`CompactGrid`] height/width estimation. //! //! [`CompactGrid`]: crate::grid::compact::CompactGrid use core::cmp::max; use crate::{ dimension::{Dimension, Estimate}, records::{IntoRecords, Records}, util::string::{count_lines, get_text_width}, }; use crate::config::compact::CompactConfig; /// A [`Dimension`] implementation which calculates exact column/row width/height. /// /// [`Grid`]: crate::grid::iterable::Grid #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct CompactGridDimension { height: usize, width: Vec, } impl CompactGridDimension { /// Calculates height of rows. pub fn height(records: R, cfg: &CompactConfig) -> Vec where R: Records, ::Cell: AsRef, { build_height(records, cfg) } /// Calculates width of columns. pub fn width(records: R, cfg: &CompactConfig) -> Vec where R: Records, ::Cell: AsRef, { build_width(records, cfg) } /// Calculates dimensions of columns. pub fn dimension(records: R, cfg: &CompactConfig) -> (Vec, Vec) where R: Records, ::Cell: AsRef, { build_dims(records, cfg) } } impl Dimension for CompactGridDimension { fn get_width(&self, column: usize) -> usize { self.width[column] } fn get_height(&self, _: usize) -> usize { self.height } } impl Estimate for CompactGridDimension where R: Records, ::Cell: AsRef, { fn estimate(&mut self, records: R, cfg: &CompactConfig) { self.width = build_width(records, cfg); let pad = cfg.get_padding(); self.height = 1 + pad.top.size + pad.bottom.size; } } fn build_dims(records: R, cfg: &CompactConfig) -> (Vec, Vec) where R: Records, ::Cell: AsRef, { let mut heights = vec![]; let mut widths = vec![0; records.count_columns()]; for columns in records.iter_rows() { let mut row_height = 0; for (col, cell) in columns.into_iter().enumerate() { let height = get_cell_height(cell.as_ref(), cfg); let width = get_cell_width(cell.as_ref(), cfg); row_height = max(row_height, height); widths[col] = max(widths[col], width) } heights.push(row_height); } (widths, heights) } fn build_height(records: R, cfg: &CompactConfig) -> Vec where R: Records, ::Cell: AsRef, { let mut heights = vec![]; for columns in records.iter_rows() { let mut row_height = 0; for cell in columns.into_iter() { let height = get_cell_height(cell.as_ref(), cfg); row_height = max(row_height, height); } heights.push(row_height); } heights } fn build_width(records: R, cfg: &CompactConfig) -> Vec where R: Records, ::Cell: AsRef, { let mut widths = vec![0; records.count_columns()]; for columns in records.iter_rows() { for (col, cell) in columns.into_iter().enumerate() { let width = get_cell_width(cell.as_ref(), cfg); widths[col] = max(widths[col], width); } } widths } fn get_cell_height(cell: &str, cfg: &CompactConfig) -> usize { let count_lines = max(1, count_lines(cell)); let pad = cfg.get_padding(); count_lines + pad.top.size + pad.bottom.size } fn get_cell_width(text: &str, cfg: &CompactConfig) -> usize { let width = get_text_width(text); let pad = cfg.get_padding(); width + pad.left.size + pad.right.size } papergrid-0.14.0/src/dimension/mod.rs000064400000000000000000000017431046102023000155710ustar 00000000000000//! The module contains an [`Dimension`] trait and its implementations. #[cfg(feature = "std")] pub mod compact; #[cfg(feature = "std")] pub mod spanned; #[cfg(feature = "std")] pub mod spanned_vec_records; /// Dimension of a [`Grid`] /// /// It's a friend trait of [`Estimate`]. /// /// [`Grid`]: crate::grid::iterable::Grid pub trait Dimension { /// Get a column width by index. fn get_width(&self, column: usize) -> usize; /// Get a row height by index. fn get_height(&self, row: usize) -> usize; } impl Dimension for &T where T: Dimension, { fn get_height(&self, row: usize) -> usize { T::get_height(self, row) } fn get_width(&self, column: usize) -> usize { T::get_width(self, column) } } /// Dimension estimation of a [`Grid`] /// /// It's a friend trait of [`Dimension`]. /// /// [`Grid`]: crate::grid::iterable::Grid pub trait Estimate { /// Estimates a metric. fn estimate(&mut self, records: R, config: &C); } papergrid-0.14.0/src/dimension/spanned.rs000064400000000000000000000242751046102023000164470ustar 00000000000000//! The module contains a [`SpannedGridDimension`] for [`Grid`] height/width estimation. //! //! [`Grid`]: crate::grid::iterable::Grid use std::{ cmp::{max, Ordering}, collections::HashMap, }; use crate::{ config::Position, dimension::{Dimension, Estimate}, records::{IntoRecords, Records}, util::string::{count_lines, get_text_dimension, get_text_width}, }; use crate::config::spanned::SpannedConfig; /// A [`Dimension`] implementation which calculates exact column/row width/height. /// /// [`Grid`]: crate::grid::iterable::Grid #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct SpannedGridDimension { height: Vec, width: Vec, } impl SpannedGridDimension { /// Calculates height of rows. pub fn height(records: R, cfg: &SpannedConfig) -> Vec where R: Records, ::Cell: AsRef, { build_height(records, cfg) } /// Calculates width of columns. pub fn width(records: R, cfg: &SpannedConfig) -> Vec where R: Records, ::Cell: AsRef, { build_width(records, cfg) } /// Calculates width of columns. pub fn width_total(records: R, cfg: &SpannedConfig) -> usize where R: Records, ::Cell: AsRef, { get_width_total(records, cfg) } /// Calculates height of rows. pub fn height_total(records: R, cfg: &SpannedConfig) -> usize where R: Records, ::Cell: AsRef, { get_height_total(records, cfg) } /// Return width and height lists. pub fn get_values(self) -> (Vec, Vec) { (self.width, self.height) } } impl Dimension for SpannedGridDimension { fn get_width(&self, column: usize) -> usize { self.width[column] } fn get_height(&self, row: usize) -> usize { self.height[row] } } impl Estimate for SpannedGridDimension where R: Records, ::Cell: AsRef, { fn estimate(&mut self, records: R, cfg: &SpannedConfig) { let (width, height) = build_dimensions(records, cfg); self.width = width; self.height = height; } } fn build_dimensions(records: R, cfg: &SpannedConfig) -> (Vec, Vec) where R: Records, ::Cell: AsRef, { let count_columns = records.count_columns(); let mut widths = vec![0; count_columns]; let mut heights = vec![]; let mut vspans = HashMap::new(); let mut hspans = HashMap::new(); for (row, columns) in records.iter_rows().into_iter().enumerate() { let mut row_height = 0; for (col, cell) in columns.into_iter().enumerate() { let pos = (row, col).into(); if !cfg.is_cell_visible(pos) { continue; } let text = cell.as_ref(); let (height, width) = get_text_dimension(text); let pad = cfg.get_padding(pos); let width = width + pad.left.size + pad.right.size; let height = height + pad.top.size + pad.bottom.size; match cfg.get_column_span(pos) { Some(n) if n > 1 => { vspans.insert(pos, (n, width)); } _ => widths[col] = max(widths[col], width), } match cfg.get_row_span(pos) { Some(n) if n > 1 => { hspans.insert(pos, (n, height)); } _ => row_height = max(row_height, height), } } heights.push(row_height); } let count_rows = heights.len(); adjust_vspans(cfg, count_columns, &vspans, &mut widths); adjust_hspans(cfg, count_rows, &hspans, &mut heights); (widths, heights) } fn adjust_hspans( cfg: &SpannedConfig, len: usize, spans: &HashMap, heights: &mut [usize], ) { if spans.is_empty() { return; } let mut spans_ordered = spans.iter().map(|(k, v)| (k, *v)).collect::>(); spans_ordered.sort_unstable_by(|(arow, acol), (brow, bcol)| match arow.cmp(brow) { Ordering::Equal => acol.cmp(bcol), ord => ord, }); for (pos, (span, height)) in spans_ordered { adjust_row_range(cfg, height, len, pos.row(), pos.row() + span, heights); } } fn adjust_row_range( cfg: &SpannedConfig, max_span_height: usize, len: usize, start: usize, end: usize, heights: &mut [usize], ) { let range_height = range_height(cfg, len, start, end, heights); if range_height >= max_span_height { return; } inc_range(heights, max_span_height - range_height, start, end); } fn range_height( cfg: &SpannedConfig, len: usize, start: usize, end: usize, heights: &[usize], ) -> usize { let count_borders = count_horizontal_borders(cfg, len, start, end); let range_height = heights[start..end].iter().sum::(); count_borders + range_height } fn count_horizontal_borders(cfg: &SpannedConfig, len: usize, start: usize, end: usize) -> usize { (start..end) .skip(1) .filter(|&i| cfg.has_horizontal(i, len)) .count() } fn get_cell_height(cell: &str, cfg: &SpannedConfig, pos: Position) -> usize { let count_lines = max(1, count_lines(cell)); let padding = cfg.get_padding(pos); count_lines + padding.top.size + padding.bottom.size } fn inc_range(list: &mut [usize], size: usize, start: usize, end: usize) { if list.is_empty() { return; } let span = end - start; let one = size / span; let rest = size - span * one; let mut i = start; while i < end { if i == start { list[i] += one + rest; } else { list[i] += one; } i += 1; } } fn adjust_vspans( cfg: &SpannedConfig, len: usize, spans: &HashMap, widths: &mut [usize], ) { if spans.is_empty() { return; } // The overall width distribution will be different depend on the order. // // We sort spans in order to prioritize the smaller spans first. let mut spans_ordered = spans.iter().map(|(k, v)| (k, *v)).collect::>(); spans_ordered.sort_unstable_by(|a, b| match a.1 .0.cmp(&b.1 .0) { Ordering::Equal => a.0.cmp(b.0), o => o, }); for (pos, (span, width)) in spans_ordered { adjust_column_range(cfg, width, len, pos.col(), pos.col() + span, widths); } } fn adjust_column_range( cfg: &SpannedConfig, max_span_width: usize, len: usize, start: usize, end: usize, widths: &mut [usize], ) { let range_width = range_width(cfg, len, start, end, widths); if range_width >= max_span_width { return; } inc_range(widths, max_span_width - range_width, start, end); } fn get_cell_width(text: &str, cfg: &SpannedConfig, pos: Position) -> usize { let padding = get_cell_padding(cfg, pos); let width = get_text_width(text); width + padding } fn get_cell_padding(cfg: &SpannedConfig, pos: Position) -> usize { let padding = cfg.get_padding(pos); padding.left.size + padding.right.size } fn range_width( cfg: &SpannedConfig, len: usize, start: usize, end: usize, widths: &[usize], ) -> usize { let count_borders = count_vertical_borders(cfg, len, start, end); let range_width = widths[start..end].iter().sum::(); count_borders + range_width } fn count_vertical_borders(cfg: &SpannedConfig, len: usize, start: usize, end: usize) -> usize { (start..end) .skip(1) .filter(|&i| cfg.has_vertical(i, len)) .count() } fn build_height(records: R, cfg: &SpannedConfig) -> Vec where R: Records, ::Cell: AsRef, { let mut heights = vec![]; let mut hspans = HashMap::new(); for (row, columns) in records.iter_rows().into_iter().enumerate() { let mut row_height = 0; for (col, cell) in columns.into_iter().enumerate() { let pos = (row, col).into(); if !cfg.is_cell_visible(pos) { continue; } let height = get_cell_height(cell.as_ref(), cfg, pos); match cfg.get_row_span(pos) { Some(n) if n > 1 => { hspans.insert(pos, (n, height)); } _ => row_height = max(row_height, height), } } heights.push(row_height); } adjust_hspans(cfg, heights.len(), &hspans, &mut heights); heights } fn build_width(records: R, cfg: &SpannedConfig) -> Vec where R: Records, ::Cell: AsRef, { let count_columns = records.count_columns(); let mut widths = vec![0; count_columns]; let mut vspans = HashMap::new(); for (row, columns) in records.iter_rows().into_iter().enumerate() { for (col, cell) in columns.into_iter().enumerate() { let pos = (row, col).into(); if !cfg.is_cell_visible(pos) { continue; } let width = get_cell_width(cell.as_ref(), cfg, pos); match cfg.get_column_span(pos) { Some(n) if n > 1 => { vspans.insert(pos, (n, width)); } _ => widths[col] = max(widths[col], width), } } } adjust_vspans(cfg, count_columns, &vspans, &mut widths); widths } fn get_width_total(records: R, cfg: &SpannedConfig) -> usize where R: Records, ::Cell: AsRef, { let widths = build_width(records, cfg); let count_columns = widths.len(); let total = widths.into_iter().sum::(); let count_verticals = cfg.count_vertical(count_columns); total + count_verticals } fn get_height_total(records: R, cfg: &SpannedConfig) -> usize where R: Records, ::Cell: AsRef, { let heights = build_height(records, cfg); let count_rows = heights.len(); let total = heights.into_iter().sum::(); let count_horizontals = cfg.count_horizontal(count_rows); total + count_horizontals } papergrid-0.14.0/src/dimension/spanned_vec_records.rs000064400000000000000000000213051046102023000210140ustar 00000000000000//! The module contains a [`SpannedVecRecordsDimension`] for [`Grid`] height/width estimation. //! //! [`Grid`]: crate::grid::iterable::Grid use std::{ cmp::{max, Ordering}, collections::HashMap, }; use crate::{ config::Position, dimension::{Dimension, Estimate}, records::{ vec_records::{Cell, VecRecords}, Records, }, }; use crate::config::spanned::SpannedConfig; /// A [`Dimension`] implementation which calculates exact column/row width/height for [`VecRecords`]. /// /// It is a specialization of [`SpannedGridDimension`] for [`VecRecords`]. /// /// [`SpannedGridDimension`]: crate::dimension::spanned::SpannedGridDimension #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct SpannedVecRecordsDimension { height: Vec, width: Vec, } impl SpannedVecRecordsDimension { /// Calculates height of rows. pub fn height>( records: &VecRecords, cfg: &SpannedConfig, ) -> Vec { build_height(records, cfg) } /// Calculates width of columns. pub fn width>(records: &VecRecords, cfg: &SpannedConfig) -> Vec { build_width(records, cfg) } /// Return width and height lists. pub fn get_values(self) -> (Vec, Vec) { (self.width, self.height) } } impl Dimension for SpannedVecRecordsDimension { fn get_width(&self, column: usize) -> usize { self.width[column] } fn get_height(&self, row: usize) -> usize { self.height[row] } } impl Estimate<&VecRecords, SpannedConfig> for SpannedVecRecordsDimension where T: Cell + AsRef, { fn estimate(&mut self, records: &VecRecords, cfg: &SpannedConfig) { let (width, height) = build_dimensions(records, cfg); self.width = width; self.height = height; } } fn build_dimensions>( records: &VecRecords, cfg: &SpannedConfig, ) -> (Vec, Vec) { let count_columns = records.count_columns(); let mut widths = vec![0; count_columns]; let mut heights = vec![]; let mut vspans = HashMap::new(); let mut hspans = HashMap::new(); for (row, columns) in records.iter_rows().enumerate() { let mut row_height = 0; for (col, cell) in columns.iter().enumerate() { let pos = (row, col).into(); if !cfg.is_cell_visible(pos) { continue; } let width = cell.width(); let height = cell.count_lines(); let pad = cfg.get_padding(pos); let width = width + pad.left.size + pad.right.size; let height = height + pad.top.size + pad.bottom.size; match cfg.get_column_span(pos) { Some(n) if n > 1 => { vspans.insert(pos, (n, width)); } _ => widths[col] = max(widths[col], width), } match cfg.get_row_span(pos) { Some(n) if n > 1 => { hspans.insert(pos, (n, height)); } _ => row_height = max(row_height, height), } } heights.push(row_height); } let count_rows = heights.len(); adjust_vspans(cfg, count_columns, &vspans, &mut widths); adjust_hspans(cfg, count_rows, &hspans, &mut heights); (widths, heights) } fn adjust_hspans( cfg: &SpannedConfig, len: usize, spans: &HashMap, heights: &mut [usize], ) { if spans.is_empty() { return; } let mut spans_ordered = spans.iter().map(|(k, v)| (*k, *v)).collect::>(); spans_ordered.sort_unstable_by(|(arow, acol), (brow, bcol)| match arow.cmp(brow) { Ordering::Equal => acol.cmp(bcol), ord => ord, }); for (pos, (span, height)) in spans_ordered { adjust_row_range(cfg, height, len, pos.row(), pos.row() + span, heights); } } fn adjust_row_range( cfg: &SpannedConfig, max_span_height: usize, len: usize, start: usize, end: usize, heights: &mut [usize], ) { let range_height = range_height(cfg, len, start, end, heights); if range_height >= max_span_height { return; } inc_range(heights, max_span_height - range_height, start, end); } fn range_height( cfg: &SpannedConfig, len: usize, start: usize, end: usize, heights: &[usize], ) -> usize { let count_borders = count_horizontal_borders(cfg, len, start, end); let range_height = heights[start..end].iter().sum::(); count_borders + range_height } fn count_horizontal_borders(cfg: &SpannedConfig, len: usize, start: usize, end: usize) -> usize { (start..end) .skip(1) .filter(|&i| cfg.has_horizontal(i, len)) .count() } fn inc_range(list: &mut [usize], size: usize, start: usize, end: usize) { if list.is_empty() { return; } let span = end - start; let one = size / span; let rest = size - span * one; let mut i = start; while i < end { if i == start { list[i] += one + rest; } else { list[i] += one; } i += 1; } } fn adjust_vspans( cfg: &SpannedConfig, len: usize, spans: &HashMap, widths: &mut [usize], ) { if spans.is_empty() { return; } // The overall width distribution will be different depend on the order. // // We sort spans in order to prioritize the smaller spans first. let mut spans_ordered = spans.iter().map(|(k, v)| (*k, *v)).collect::>(); spans_ordered.sort_unstable_by(|a, b| match a.1 .0.cmp(&b.1 .0) { Ordering::Equal => a.0.cmp(&b.0), o => o, }); for (pos, (span, width)) in spans_ordered { adjust_column_range(cfg, width, len, pos.col(), pos.col() + span, widths); } } fn adjust_column_range( cfg: &SpannedConfig, max_span_width: usize, len: usize, start: usize, end: usize, widths: &mut [usize], ) { let range_width = range_width(cfg, len, start, end, widths); if range_width >= max_span_width { return; } inc_range(widths, max_span_width - range_width, start, end); } fn get_cell_padding_horizontal(cfg: &SpannedConfig, pos: Position) -> usize { let padding = cfg.get_padding(pos); padding.left.size + padding.right.size } fn get_cell_vertical_padding(cfg: &SpannedConfig, pos: Position) -> usize { let padding = cfg.get_padding(pos); padding.top.size + padding.bottom.size } fn range_width( cfg: &SpannedConfig, len: usize, start: usize, end: usize, widths: &[usize], ) -> usize { let count_borders = count_vertical_borders(cfg, len, start, end); let range_width = widths[start..end].iter().sum::(); count_borders + range_width } fn count_vertical_borders(cfg: &SpannedConfig, len: usize, start: usize, end: usize) -> usize { (start..end) .skip(1) .filter(|&i| cfg.has_vertical(i, len)) .count() } fn build_height>(records: &VecRecords, cfg: &SpannedConfig) -> Vec { let mut heights = vec![]; let mut hspans = HashMap::new(); for (row, columns) in records.iter_rows().enumerate() { let mut row_height = 0; for (col, cell) in columns.iter().enumerate() { let pos = (row, col).into(); if !cfg.is_cell_visible(pos) { continue; } let height = cell.count_lines() + get_cell_vertical_padding(cfg, pos); match cfg.get_row_span(pos) { Some(n) if n > 1 => { hspans.insert(pos, (n, height)); } _ => row_height = max(row_height, height), } } heights.push(row_height); } adjust_hspans(cfg, heights.len(), &hspans, &mut heights); heights } fn build_width(records: &VecRecords, cfg: &SpannedConfig) -> Vec where T: Cell + AsRef, { let count_columns = records.count_columns(); let mut widths = vec![0; count_columns]; let mut vspans = HashMap::new(); for (row, columns) in records.iter_rows().enumerate() { for (col, cell) in columns.iter().enumerate() { let pos = (row, col).into(); if !cfg.is_cell_visible(pos) { continue; } let width = cell.width() + get_cell_padding_horizontal(cfg, pos); match cfg.get_column_span(pos) { Some(n) if n > 1 => { vspans.insert(pos, (n, width)); } _ => widths[col] = max(widths[col], width), } } } adjust_vspans(cfg, count_columns, &vspans, &mut widths); widths } papergrid-0.14.0/src/grid/compact.rs000064400000000000000000000505671046102023000154100ustar 00000000000000//! The module contains a [`CompactGrid`] structure, //! which is a relatively strict grid. use core::{ borrow::Borrow, fmt::{self, Display, Write}, }; use crate::{ ansi::{ANSIFmt, ANSIStr}, colors::{Colors, NoColors}, config::{AlignmentHorizontal, Borders, HorizontalLine, Indent, Sides}, dimension::Dimension, records::{IntoRecords, Records}, util::string::get_line_width, }; use crate::config::compact::CompactConfig; /// Grid provides a set of methods for building a text-based table. #[derive(Debug, Clone)] pub struct CompactGrid { records: R, config: G, dimension: D, colors: C, } impl CompactGrid { /// The new method creates a grid instance with default styles. pub fn new(records: R, dimension: D, config: G) -> Self { CompactGrid { records, config, dimension, colors: NoColors, } } } impl CompactGrid { /// Sets colors map. pub fn with_colors(self, colors: Colors) -> CompactGrid { CompactGrid { records: self.records, config: self.config, dimension: self.dimension, colors, } } /// Builds a table. pub fn build(self, mut f: F) -> fmt::Result where R: Records, ::Cell: AsRef, D: Dimension, C: Colors, G: Borrow, F: Write, { if self.records.count_columns() == 0 { return Ok(()); } let config = self.config.borrow(); print_grid(&mut f, self.records, config, &self.dimension, &self.colors) } /// Builds a table into string. /// /// Notice that it consumes self. #[cfg(feature = "std")] #[allow(clippy::inherent_to_string)] pub fn to_string(self) -> String where R: Records, ::Cell: AsRef, D: Dimension, G: Borrow, C: Colors, { let mut buf = String::new(); self.build(&mut buf).expect("It's guaranteed to never happen otherwise it's considered an stdlib error or impl error"); buf } } impl Display for CompactGrid where for<'a> &'a R: Records, for<'a> <<&'a R as Records>::Iter as IntoRecords>::Cell: AsRef, D: Dimension, G: Borrow, C: Colors, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let records = &self.records; let config = self.config.borrow(); print_grid(f, records, config, &self.dimension, &self.colors) } } fn print_grid( f: &mut F, records: R, cfg: &CompactConfig, dims: &D, colors: &C, ) -> fmt::Result where F: Write, R: Records, ::Cell: AsRef, D: Dimension, C: Colors, { let count_columns = records.count_columns(); let count_rows = records.hint_count_rows(); if count_columns == 0 || matches!(count_rows, Some(0)) { return Ok(()); } let mut records = records.iter_rows().into_iter(); let records_first = match records.next() { Some(row) => row, None => return Ok(()), }; let wtotal = total_width(cfg, dims, count_columns); let borders_chars = cfg.get_borders(); let borders_colors = cfg.get_borders_color(); let horizontal_borders = create_horizontal(borders_chars); let horizontal_colors = create_horizontal_colors(borders_colors); let vertical_borders = create_vertical_borders(borders_chars, borders_colors); let margin = create_margin(cfg); let padding = create_padding(cfg); let alignment = cfg.get_alignment_horizontal(); let mut new_line = false; if margin.top.space.size > 0 { let width_total = wtotal + margin.left.space.size + margin.right.space.size; let indent = ColoredIndent::new(width_total, margin.top.space.fill, margin.top.color); print_indent_lines(f, indent)?; new_line = true; } if borders_chars.has_top() { if new_line { f.write_char('\n')? } let borders = create_horizontal_top(borders_chars); let borders_colors = create_horizontal_top_colors(borders_colors); print_horizontal_line(f, dims, &borders, &borders_colors, &margin, count_columns)?; new_line = true; } if borders_chars.has_horizontal() { if new_line { f.write_char('\n')?; } let cells = records_first.into_iter(); print_grid_row( f, cells, count_columns, dims, colors, &margin, &padding, &vertical_borders, alignment, 0, )?; for (row, cells) in records.enumerate() { f.write_char('\n')?; print_horizontal_line( f, dims, &horizontal_borders, &horizontal_colors, &margin, count_columns, )?; f.write_char('\n')?; let cells = cells.into_iter(); print_grid_row( f, cells, count_columns, dims, colors, &margin, &padding, &vertical_borders, alignment, row + 1, )?; } } else { if new_line { f.write_char('\n')?; } print_grid_row( f, records_first.into_iter(), count_columns, dims, colors, &margin, &padding, &vertical_borders, alignment, 0, )?; for (row, cells) in records.enumerate() { f.write_char('\n')?; print_grid_row( f, cells.into_iter(), count_columns, dims, colors, &margin, &padding, &vertical_borders, alignment, row + 1, )?; } } if borders_chars.has_bottom() { f.write_char('\n')?; let borders = create_horizontal_bottom(borders_chars); let colors = create_horizontal_bottom_colors(borders_colors); print_horizontal_line(f, dims, &borders, &colors, &margin, count_columns)?; } if cfg.get_margin().bottom.size > 0 { f.write_char('\n')?; let width_total = wtotal + margin.left.space.size + margin.right.space.size; let indent = ColoredIndent::new(width_total, margin.bottom.space.fill, margin.bottom.color); print_indent_lines(f, indent)?; } Ok(()) } fn create_margin(cfg: &CompactConfig) -> Sides { let margin = cfg.get_margin(); let margin_color = cfg.get_margin_color(); Sides::new( ColoredIndent::from_indent(margin.left, margin_color.left), ColoredIndent::from_indent(margin.right, margin_color.right), ColoredIndent::from_indent(margin.top, margin_color.top), ColoredIndent::from_indent(margin.bottom, margin_color.bottom), ) } fn create_vertical_borders( borders: &Borders, colors: &Borders>, ) -> HorizontalLine { let intersect = borders .vertical .map(|c| ColoredIndent::new(0, c, colors.vertical)); let left = borders.left.map(|c| ColoredIndent::new(0, c, colors.left)); let right = borders .right .map(|c| ColoredIndent::new(0, c, colors.right)); HorizontalLine::new(None, intersect, left, right) } fn print_horizontal_line( f: &mut F, dims: &D, borders: &HorizontalLine, borders_colors: &HorizontalLine>, margin: &Sides, count_columns: usize, ) -> fmt::Result where F: fmt::Write, D: Dimension, { let is_not_colored = borders_colors.is_empty(); print_indent(f, margin.left)?; if is_not_colored { print_split_line(f, dims, borders, count_columns)?; } else { print_split_line_colored(f, dims, borders, borders_colors, count_columns)?; } print_indent(f, margin.right)?; Ok(()) } #[allow(clippy::too_many_arguments)] fn print_grid_row( f: &mut F, data: I, size: usize, dims: &D, colors: &C, margin: &Sides, padding: &Sides, borders: &HorizontalLine, alignment: AlignmentHorizontal, row: usize, ) -> fmt::Result where F: Write, I: Iterator, T: AsRef, C: Colors, D: Dimension, { for _ in 0..padding.top.space.size { print_indent(f, margin.left)?; print_columns_empty_colored(f, dims, borders, padding.top.color, size)?; print_indent(f, margin.right)?; f.write_char('\n')?; } print_indent(f, margin.left)?; print_row_columns_one_line(f, data, dims, colors, borders, padding, alignment, row)?; print_indent(f, margin.right)?; for _ in 0..padding.top.space.size { f.write_char('\n')?; print_indent(f, margin.left)?; print_columns_empty_colored(f, dims, borders, padding.bottom.color, size)?; print_indent(f, margin.right)?; } Ok(()) } fn create_padding(cfg: &CompactConfig) -> Sides { let pad = cfg.get_padding(); let colors = cfg.get_padding_color(); Sides::new( ColoredIndent::new(pad.left.size, pad.left.fill, create_color(colors.left)), ColoredIndent::new(pad.right.size, pad.right.fill, create_color(colors.right)), ColoredIndent::new(pad.top.size, pad.top.fill, create_color(colors.top)), ColoredIndent::new( pad.bottom.size, pad.bottom.fill, create_color(colors.bottom), ), ) } fn create_horizontal(b: &Borders) -> HorizontalLine { HorizontalLine::new(b.horizontal, b.intersection, b.left, b.right) } fn create_horizontal_top(b: &Borders) -> HorizontalLine { HorizontalLine::new(b.top, b.top_intersection, b.top_left, b.top_right) } fn create_horizontal_bottom(b: &Borders) -> HorizontalLine { HorizontalLine::new( b.bottom, b.bottom_intersection, b.bottom_left, b.bottom_right, ) } fn create_horizontal_colors(b: &Borders>) -> HorizontalLine> { HorizontalLine::new(b.horizontal, b.intersection, b.left, b.right) } fn create_horizontal_top_colors(b: &Borders>) -> HorizontalLine> { HorizontalLine::new(b.top, b.top_intersection, b.top_left, b.top_right) } fn create_horizontal_bottom_colors( b: &Borders>, ) -> HorizontalLine> { HorizontalLine::new( b.bottom, b.bottom_intersection, b.bottom_left, b.bottom_right, ) } fn total_width(cfg: &CompactConfig, dims: &D, count_columns: usize) -> usize where D: Dimension, { let content_width = total_columns_width(count_columns, dims); let count_verticals = count_verticals(cfg, count_columns); content_width + count_verticals } fn total_columns_width(count_columns: usize, dims: &D) -> usize where D: Dimension, { (0..count_columns).map(|i| dims.get_width(i)).sum::() } fn count_verticals(cfg: &CompactConfig, count_columns: usize) -> usize { assert!(count_columns > 0); let count_verticals = count_columns - 1; let borders = cfg.get_borders(); borders.has_vertical() as usize * count_verticals + borders.has_left() as usize + borders.has_right() as usize } #[allow(clippy::too_many_arguments)] fn print_row_columns_one_line( f: &mut F, mut data: I, dims: &D, colors: &C, borders: &HorizontalLine, padding: &Sides, alignment: AlignmentHorizontal, row: usize, ) -> fmt::Result where F: Write, I: Iterator, T: AsRef, D: Dimension, C: Colors, { if let Some(indent) = borders.left { print_char(f, indent.space.fill, indent.color)?; } let text = data .next() .expect("we check in the beginning that size must be at least 1 column"); let width = dims.get_width(0); let color = colors.get_color((row, 0).into()); let text = text.as_ref(); let text = text.lines().next().unwrap_or(""); print_cell(f, text, color, padding, alignment, width)?; match borders.intersection { Some(indent) => { for (col, text) in data.enumerate() { let col = col + 1; let width = dims.get_width(col); let color = colors.get_color((row, col).into()); let text = text.as_ref(); let text = text.lines().next().unwrap_or(""); print_char(f, indent.space.fill, indent.color)?; print_cell(f, text, color, padding, alignment, width)?; } } None => { for (col, text) in data.enumerate() { let col = col + 1; let width = dims.get_width(col); let color = colors.get_color((row, col).into()); let text = text.as_ref(); let text = text.lines().next().unwrap_or(""); print_cell(f, text, color, padding, alignment, width)?; } } } if let Some(indent) = borders.right { print_char(f, indent.space.fill, indent.color)?; } Ok(()) } fn print_columns_empty_colored( f: &mut F, dims: &D, borders: &HorizontalLine, color: Option>, count_columns: usize, ) -> fmt::Result where F: Write, D: Dimension, { if let Some(indent) = borders.left { print_char(f, indent.space.fill, indent.color)?; } let width = dims.get_width(0); print_indent(f, ColoredIndent::new(width, ' ', color))?; match borders.intersection { Some(indent) => { for column in 1..count_columns { let width = dims.get_width(column); print_char(f, indent.space.fill, indent.color)?; print_indent(f, ColoredIndent::new(width, ' ', color))?; } } None => { for column in 1..count_columns { let width = dims.get_width(column); print_indent(f, ColoredIndent::new(width, ' ', color))?; } } } if let Some(indent) = borders.right { print_char(f, indent.space.fill, indent.color)?; } Ok(()) } fn print_cell( f: &mut F, text: &str, color: Option, padding: &Sides, alignment: AlignmentHorizontal, width: usize, ) -> fmt::Result where F: Write, C: ANSIFmt, { let available = width - (padding.left.space.size + padding.right.space.size); let text_width = get_line_width(text); let (left, right) = if available > text_width { calculate_indent(alignment, text_width, available) } else { (0, 0) }; print_indent(f, padding.left)?; repeat_char(f, ' ', left)?; print_text(f, text, color)?; repeat_char(f, ' ', right)?; print_indent(f, padding.right)?; Ok(()) } fn print_split_line_colored( f: &mut F, dimension: &D, borders: &HorizontalLine, borders_colors: &HorizontalLine>, count_columns: usize, ) -> fmt::Result where F: Write, D: Dimension, { let mut used_color = ANSIStr::default(); let chars_main = borders.main.unwrap_or(' '); if let Some(c) = borders.left { if let Some(color) = &borders_colors.right { prepare_coloring(f, color, &mut used_color)?; } f.write_char(c)?; } let width = dimension.get_width(0); if width > 0 { if let Some(color) = borders_colors.main { prepare_coloring(f, &color, &mut used_color)?; } repeat_char(f, chars_main, width)?; } for col in 1..count_columns { if let Some(c) = borders.intersection { if let Some(color) = borders_colors.intersection { prepare_coloring(f, &color, &mut used_color)?; } f.write_char(c)?; } let width = dimension.get_width(col); if width > 0 { if let Some(color) = borders_colors.main { prepare_coloring(f, &color, &mut used_color)?; } repeat_char(f, chars_main, width)?; } } if let Some(c) = borders.right { if let Some(color) = &borders_colors.right { prepare_coloring(f, color, &mut used_color)?; } f.write_char(c)?; } used_color.fmt_ansi_suffix(f)?; Ok(()) } fn print_split_line( f: &mut F, dims: &D, chars: &HorizontalLine, count_columns: usize, ) -> fmt::Result where F: Write, D: Dimension, { let chars_main = chars.main.unwrap_or(' '); if let Some(c) = chars.left { f.write_char(c)?; } let width = dims.get_width(0); if width > 0 { repeat_char(f, chars_main, width)?; } for col in 1..count_columns { if let Some(c) = chars.intersection { f.write_char(c)?; } let width = dims.get_width(col); if width > 0 { repeat_char(f, chars_main, width)?; } } if let Some(c) = chars.right { f.write_char(c)?; } Ok(()) } fn print_text(f: &mut F, text: &str, color: Option) -> fmt::Result where F: Write, C: ANSIFmt, { match color { Some(color) => { color.fmt_ansi_prefix(f)?; f.write_str(text)?; color.fmt_ansi_suffix(f)?; } None => { f.write_str(text)?; } }; Ok(()) } fn prepare_coloring( f: &mut F, clr: &ANSIStr<'static>, used: &mut ANSIStr<'static>, ) -> fmt::Result where F: Write, { if *used != *clr { used.fmt_ansi_suffix(f)?; clr.fmt_ansi_prefix(f)?; *used = *clr; } Ok(()) } fn calculate_indent( alignment: AlignmentHorizontal, text_width: usize, available: usize, ) -> (usize, usize) { let diff = available - text_width; match alignment { AlignmentHorizontal::Left => (0, diff), AlignmentHorizontal::Right => (diff, 0), AlignmentHorizontal::Center => { let left = diff / 2; let rest = diff - left; (left, rest) } } } fn repeat_char(f: &mut F, c: char, n: usize) -> fmt::Result where F: Write, { for _ in 0..n { f.write_char(c)?; } Ok(()) } // todo: replace Option to StaticColor and check performance fn print_char(f: &mut F, c: char, color: Option>) -> fmt::Result where F: Write, { match color { Some(color) => { color.fmt_ansi_prefix(f)?; f.write_char(c)?; color.fmt_ansi_suffix(f) } None => f.write_char(c), } } fn print_indent_lines(f: &mut F, indent: ColoredIndent) -> fmt::Result where F: Write, { print_indent(f, indent)?; f.write_char('\n')?; for _ in 1..indent.space.size { f.write_char('\n')?; print_indent(f, indent)?; } Ok(()) } fn print_indent(f: &mut F, indent: ColoredIndent) -> fmt::Result where F: Write, { match indent.color { Some(color) => { color.fmt_ansi_prefix(f)?; repeat_char(f, indent.space.fill, indent.space.size)?; color.fmt_ansi_suffix(f)?; } None => { repeat_char(f, indent.space.fill, indent.space.size)?; } } Ok(()) } #[derive(Debug, Clone, Copy)] struct ColoredIndent { space: Indent, color: Option>, } impl ColoredIndent { fn new(width: usize, c: char, color: Option>) -> Self { Self { space: Indent::new(width, c), color, } } fn from_indent(indent: Indent, color: ANSIStr<'static>) -> Self { Self { space: indent, color: create_color(color), } } } fn create_color(color: ANSIStr<'static>) -> Option> { if color.is_empty() { None } else { Some(color) } } papergrid-0.14.0/src/grid/iterable.rs000064400000000000000000001063521046102023000155430ustar 00000000000000//! The module contains a [`Grid`] structure. use std::{ borrow::{Borrow, Cow}, cmp, collections::BTreeMap, fmt::{self, Write}, }; use crate::{ ansi::{ANSIBuf, ANSIFmt}, colors::Colors, config::{ spanned::{Offset, SpannedConfig}, AlignmentHorizontal, AlignmentVertical, Formatting, Indent, Position, Sides, }, dimension::Dimension, records::{IntoRecords, Records}, util::string::{count_lines, get_line_width, get_lines, get_text_width, Lines}, }; /// Grid provides a set of methods for building a text-based table. #[derive(Debug, Clone)] pub struct Grid { records: R, config: G, dimension: D, colors: C, } impl Grid { /// The new method creates a grid instance with default styles. pub fn new(records: R, dimension: D, config: G, colors: C) -> Self { Grid { records, config, dimension, colors, } } } impl Grid { /// Builds a table. pub fn build(self, mut f: F) -> fmt::Result where R: Records, ::Cell: AsRef, D: Dimension, C: Colors, G: Borrow, F: Write, { if self.records.count_columns() == 0 || self.records.hint_count_rows() == Some(0) { return Ok(()); } let config = self.config.borrow(); print_grid(&mut f, self.records, config, &self.dimension, &self.colors) } /// Builds a table into string. /// /// Notice that it consumes self. #[allow(clippy::inherent_to_string)] pub fn to_string(self) -> String where R: Records, ::Cell: AsRef, D: Dimension, G: Borrow, C: Colors, { let mut buf = String::new(); self.build(&mut buf).expect("It's guaranteed to never happen otherwise it's considered an stdlib error or impl error"); buf } } fn print_grid( f: &mut F, records: R, cfg: &SpannedConfig, dimension: &D, colors: &C, ) -> fmt::Result where F: Write, R: Records, ::Cell: AsRef, D: Dimension, C: Colors, { // spanned version is a bit more complex and 'supposedly' slower, // because spans are considered to be not a general case we are having 2 versions let grid_has_spans = cfg.has_column_spans() || cfg.has_row_spans(); if grid_has_spans { print_grid_spanned(f, records, cfg, dimension, colors) } else { print_grid_general(f, records, cfg, dimension, colors) } } fn print_grid_general( f: &mut F, records: R, cfg: &SpannedConfig, dims: &D, colors: &C, ) -> fmt::Result where F: Write, R: Records, ::Cell: AsRef, D: Dimension, C: Colors, { let count_columns = records.count_columns(); let mut totalw = None; let totalh = records .hint_count_rows() .map(|count_rows| total_height(cfg, dims, count_rows)); let mut records_iter = records.iter_rows().into_iter(); let mut next_columns = records_iter.next(); if next_columns.is_none() { return Ok(()); } if cfg.get_margin().top.size > 0 { totalw = Some(output_width(cfg, dims, count_columns)); print_margin_top(f, cfg, totalw.unwrap())?; f.write_char('\n')?; } let mut row = 0; let mut line = 0; let mut is_prev_row_skipped = false; let mut buf = None; while let Some(columns) = next_columns { let columns = columns.into_iter(); next_columns = records_iter.next(); let is_last_row = next_columns.is_none(); let height = dims.get_height(row); let count_rows = convert_count_rows(row, is_last_row); let has_horizontal = cfg.has_horizontal(row, count_rows); let shape = (count_rows, count_columns); if row > 0 && !is_prev_row_skipped && (has_horizontal || height > 0) { f.write_char('\n')?; } if has_horizontal { print_horizontal_line(f, cfg, line, totalh, dims, row, shape)?; line += 1; if height > 0 { f.write_char('\n')?; } } if height == 1 { print_single_line_columns(f, columns, cfg, colors, dims, row, line, totalh, shape)? } else if height > 0 { if buf.is_none() { buf = Some(Vec::with_capacity(count_columns)); } let buf = buf.as_mut().unwrap(); print_multiline_columns( f, columns, cfg, colors, dims, height, row, line, totalh, shape, buf, )?; buf.clear(); } is_prev_row_skipped = height == 0 && !has_horizontal; line += height; row += 1; } if cfg.has_horizontal(row, row) { f.write_char('\n')?; let shape = (row, count_columns); print_horizontal_line(f, cfg, line, totalh, dims, row, shape)?; } { let margin = cfg.get_margin(); if margin.bottom.size > 0 { let totalw = totalw.unwrap_or_else(|| output_width(cfg, dims, count_columns)); f.write_char('\n')?; print_margin_bottom(f, cfg, totalw)?; } } Ok(()) } fn output_width(cfg: &SpannedConfig, d: D, count_columns: usize) -> usize { let margin = cfg.get_margin(); total_width(cfg, &d, count_columns) + margin.left.size + margin.right.size } #[allow(clippy::too_many_arguments)] fn print_horizontal_line( f: &mut F, cfg: &SpannedConfig, line: usize, totalh: Option, dimension: &D, row: usize, shape: (usize, usize), ) -> fmt::Result { print_margin_left(f, cfg, line, totalh)?; print_split_line(f, cfg, dimension, row, shape)?; print_margin_right(f, cfg, line, totalh)?; Ok(()) } #[allow(clippy::too_many_arguments)] fn print_multiline_columns<'a, F, I, D, C>( f: &mut F, columns: I, cfg: &'a SpannedConfig, colors: &'a C, dimension: &D, height: usize, row: usize, line: usize, totalh: Option, shape: (usize, usize), buf: &mut Vec>, ) -> fmt::Result where F: Write, I: Iterator, I::Item: AsRef, D: Dimension, C: Colors, { collect_columns(buf, columns, cfg, colors, dimension, height, row); print_columns_lines(f, buf, height, cfg, line, row, totalh, shape)?; Ok(()) } #[allow(clippy::too_many_arguments)] fn print_single_line_columns( f: &mut F, columns: I, cfg: &SpannedConfig, colors: &C, dims: &D, row: usize, line: usize, totalh: Option, shape: (usize, usize), ) -> fmt::Result where F: Write, I: Iterator, I::Item: AsRef, D: Dimension, C: Colors, { print_margin_left(f, cfg, line, totalh)?; for (col, cell) in columns.enumerate() { let pos = (row, col).into(); let width = dims.get_width(col); let color = colors.get_color(pos); print_vertical_char(f, cfg, pos, 0, 1, shape.1)?; print_single_line_column(f, cell.as_ref(), cfg, width, color, pos)?; } print_vertical_char(f, cfg, (row, shape.1).into(), 0, 1, shape.1)?; print_margin_right(f, cfg, line, totalh)?; Ok(()) } fn print_single_line_column( f: &mut F, text: &str, cfg: &SpannedConfig, width: usize, color: Option<&C>, pos: Position, ) -> fmt::Result { let pad = cfg.get_padding(pos); let pad_color = cfg.get_padding_color(pos); let fmt = cfg.get_formatting(pos); let space = cfg.get_justification(pos); let space_color = cfg.get_justification_color(pos); let (text, text_width) = if fmt.horizontal_trim && !text.is_empty() { let text = string_trim(text); let width = get_line_width(&text); (text, width) } else { let text = Cow::Borrowed(text); let width = get_text_width(&text); (text, width) }; let alignment = *cfg.get_alignment_horizontal(pos); let available_width = width - pad.left.size - pad.right.size; let (left, right) = calculate_indent(alignment, text_width, available_width); print_padding(f, &pad.left, pad_color.left.as_ref())?; print_indent(f, space, left, space_color)?; print_text(f, &text, color)?; print_indent(f, space, right, space_color)?; print_padding(f, &pad.right, pad_color.right.as_ref())?; Ok(()) } #[allow(clippy::too_many_arguments)] fn print_columns_lines( f: &mut F, buf: &mut [Cell], height: usize, cfg: &SpannedConfig, line: usize, row: usize, totalh: Option, shape: (usize, usize), ) -> fmt::Result { for i in 0..height { let exact_line = line + i; print_margin_left(f, cfg, exact_line, totalh)?; for (col, cell) in buf.iter_mut().enumerate() { print_vertical_char(f, cfg, (row, col).into(), i, height, shape.1)?; cell.display(f)?; } print_vertical_char(f, cfg, (row, shape.1).into(), i, height, shape.1)?; print_margin_right(f, cfg, exact_line, totalh)?; if i + 1 != height { f.write_char('\n')?; } } Ok(()) } fn collect_columns<'a, I, D, C>( buf: &mut Vec>, iter: I, cfg: &SpannedConfig, colors: &'a C, dimension: &D, height: usize, row: usize, ) where I: Iterator, I::Item: AsRef, C: Colors, D: Dimension, { let iter = iter.enumerate().map(|(col, cell)| { let pos = (row, col).into(); let width = dimension.get_width(col); let color = colors.get_color(pos); Cell::new(cell, width, height, cfg, color, pos) }); buf.extend(iter); } fn print_split_line( f: &mut F, cfg: &SpannedConfig, dimension: &D, row: usize, shape: (usize, usize), ) -> fmt::Result { let mut used_color = None; print_vertical_intersection(f, cfg, (row, 0).into(), shape, &mut used_color)?; for col in 0..shape.1 { let width = dimension.get_width(col); // general case if width > 0 { let pos = (row, col).into(); let main = cfg.get_horizontal(pos, shape.0); match main { Some(c) => { let clr = cfg.get_horizontal_color(pos, shape.0); prepare_coloring(f, clr, &mut used_color)?; print_horizontal_border(f, cfg, pos, width, c, &used_color)?; } None => repeat_char(f, ' ', width)?, } } print_vertical_intersection(f, cfg, (row, col + 1).into(), shape, &mut used_color)?; } if let Some(clr) = used_color.take() { clr.fmt_ansi_suffix(f)?; } Ok(()) } fn print_grid_spanned( f: &mut F, records: R, cfg: &SpannedConfig, dims: &D, colors: &C, ) -> fmt::Result where F: Write, R: Records, ::Cell: AsRef, D: Dimension, C: Colors, { let count_columns = records.count_columns(); let total_width = total_width(cfg, dims, count_columns); let margin = cfg.get_margin(); let total_width_with_margin = total_width + margin.left.size + margin.right.size; let totalh = records .hint_count_rows() .map(|rows| total_height(cfg, dims, rows)); if margin.top.size > 0 { print_margin_top(f, cfg, total_width_with_margin)?; f.write_char('\n')?; } let mut buf = BTreeMap::new(); let mut records_iter = records.iter_rows().into_iter(); let mut next_columns = records_iter.next(); let mut need_new_line = false; let mut line = 0; let mut row = 0; while let Some(columns) = next_columns { let columns = columns.into_iter(); next_columns = records_iter.next(); let is_last_row = next_columns.is_none(); let height = dims.get_height(row); let count_rows = convert_count_rows(row, is_last_row); let shape = (count_rows, count_columns); let has_horizontal = cfg.has_horizontal(row, count_rows); if need_new_line && (has_horizontal || height > 0) { f.write_char('\n')?; need_new_line = false; } if has_horizontal { print_margin_left(f, cfg, line, totalh)?; print_split_line_spanned(f, &mut buf, cfg, dims, row, shape)?; print_margin_right(f, cfg, line, totalh)?; line += 1; if height > 0 { f.write_char('\n')?; } } print_spanned_columns( f, &mut buf, columns, cfg, colors, dims, height, row, line, totalh, shape, )?; if has_horizontal || height > 0 { need_new_line = true; } line += height; row += 1; } if row > 0 { if cfg.has_horizontal(row, row) { f.write_char('\n')?; let shape = (row, count_columns); print_horizontal_line(f, cfg, line, totalh, dims, row, shape)?; } if margin.bottom.size > 0 { f.write_char('\n')?; print_margin_bottom(f, cfg, total_width_with_margin)?; } } Ok(()) } fn print_split_line_spanned( f: &mut F, buf: &mut BTreeMap, usize, usize)>, cfg: &SpannedConfig, dimension: &D, row: usize, shape: (usize, usize), ) -> fmt::Result { let mut used_color = None; print_vertical_intersection(f, cfg, (row, 0).into(), shape, &mut used_color)?; for col in 0..shape.1 { let pos = (row, col).into(); if cfg.is_cell_covered_by_both_spans(pos) { continue; } let width = dimension.get_width(col); let mut col = col; if cfg.is_cell_covered_by_row_span(pos) { // means it's part of other a spanned cell // so. we just need to use line from other cell. let (cell, _, _) = buf.get_mut(&col).unwrap(); cell.display(f)?; // We need to use a correct right split char. let original_row = closest_visible_row(cfg, pos).unwrap(); if let Some(span) = cfg.get_column_span((original_row, col).into()) { col += span - 1; } } else if width > 0 { // general case let main = cfg.get_horizontal(pos, shape.0); match main { Some(c) => { let clr = cfg.get_horizontal_color(pos, shape.0); prepare_coloring(f, clr, &mut used_color)?; print_horizontal_border(f, cfg, pos, width, c, &used_color)?; } None => repeat_char(f, ' ', width)?, } } print_vertical_intersection(f, cfg, (row, col + 1).into(), shape, &mut used_color)?; } if let Some(clr) = used_color.take() { clr.fmt_ansi_suffix(f)?; } Ok(()) } fn print_vertical_intersection<'a, F: fmt::Write>( f: &mut F, cfg: &'a SpannedConfig, pos: Position, shape: (usize, usize), used_color: &mut Option<&'a ANSIBuf>, ) -> fmt::Result { if !cfg.has_vertical(pos.col(), shape.1) { return Ok(()); } match cfg.get_intersection(pos, shape) { Some(c) => { let clr = cfg.get_intersection_color(pos, shape); prepare_coloring(f, clr, used_color)?; f.write_char(c) } None => Ok(()), } } #[allow(clippy::too_many_arguments, clippy::type_complexity)] fn print_spanned_columns<'a, F, I, D, C>( f: &mut F, buf: &mut BTreeMap, usize, usize)>, iter: I, cfg: &SpannedConfig, colors: &'a C, dimension: &D, this_height: usize, row: usize, line: usize, totalh: Option, shape: (usize, usize), ) -> fmt::Result where F: Write, I: Iterator, I::Item: AsRef, D: Dimension, C: Colors, { if this_height == 0 { // it's possible that we dont show row but it contains an actual cell which will be // rendered after all cause it's a rowspanned let mut skip = 0; for (col, cell) in iter.enumerate() { if skip > 0 { skip -= 1; continue; } if let Some((_, _, colspan)) = buf.get(&col) { skip = *colspan - 1; continue; } let pos = (row, col).into(); let rowspan = cfg.get_row_span(pos).unwrap_or(1); if rowspan < 2 { continue; } let height = if rowspan > 1 { range_height(cfg, dimension, row, row + rowspan, shape.0) } else { this_height }; let colspan = cfg.get_column_span(pos).unwrap_or(1); skip = colspan - 1; let width = if colspan > 1 { range_width(cfg, dimension, col, col + colspan, shape.1) } else { dimension.get_width(col) }; let color = colors.get_color(pos); let cell = Cell::new(cell, width, height, cfg, color, pos); buf.insert(col, (cell, rowspan, colspan)); } buf.retain(|_, (_, rowspan, _)| { *rowspan -= 1; *rowspan != 0 }); return Ok(()); } let mut skip = 0; for (col, cell) in iter.enumerate() { if skip > 0 { skip -= 1; continue; } if let Some((_, _, colspan)) = buf.get(&col) { skip = *colspan - 1; continue; } let pos = (row, col).into(); let colspan = cfg.get_column_span(pos).unwrap_or(1); skip = colspan - 1; let width = if colspan > 1 { range_width(cfg, dimension, col, col + colspan, shape.1) } else { dimension.get_width(col) }; let rowspan = cfg.get_row_span(pos).unwrap_or(1); let height = if rowspan > 1 { range_height(cfg, dimension, row, row + rowspan, shape.0) } else { this_height }; let color = colors.get_color(pos); let cell = Cell::new(cell, width, height, cfg, color, pos); buf.insert(col, (cell, rowspan, colspan)); } for i in 0..this_height { let exact_line = line + i; let cell_line = i; print_margin_left(f, cfg, exact_line, totalh)?; for (&col, (cell, _, _)) in buf.iter_mut() { print_vertical_char(f, cfg, (row, col).into(), cell_line, this_height, shape.1)?; cell.display(f)?; } print_vertical_char( f, cfg, (row, shape.1).into(), cell_line, this_height, shape.1, )?; print_margin_right(f, cfg, exact_line, totalh)?; if i + 1 != this_height { f.write_char('\n')?; } } buf.retain(|_, (_, rowspan, _)| { *rowspan -= 1; *rowspan != 0 }); Ok(()) } fn print_horizontal_border( f: &mut F, cfg: &SpannedConfig, pos: Position, width: usize, c: char, used_color: &Option<&ANSIBuf>, ) -> fmt::Result { if !cfg.is_overridden_horizontal(pos) { return repeat_char(f, c, width); } for i in 0..width { let c = cfg.lookup_horizontal_char(pos, i, width).unwrap_or(c); match cfg.lookup_horizontal_color(pos, i, width) { Some(color) => match used_color { Some(clr) => { clr.fmt_ansi_suffix(f)?; color.fmt_ansi_prefix(f)?; f.write_char(c)?; color.fmt_ansi_suffix(f)?; clr.fmt_ansi_prefix(f)?; } None => { color.fmt_ansi_prefix(f)?; f.write_char(c)?; color.fmt_ansi_suffix(f)?; } }, _ => f.write_char(c)?, } } Ok(()) } struct Cell { lines: LinesIter, width: usize, indent_top: usize, indent_left: Option, alignh: AlignmentHorizontal, fmt: Formatting, pad: Sides, pad_color: Sides>, color: Option, justification: (char, Option), } impl Cell where T: AsRef, { fn new( text: T, width: usize, height: usize, cfg: &SpannedConfig, color: Option, pos: Position, ) -> Cell { let fmt = cfg.get_formatting(pos); let pad = cfg.get_padding(pos); let pad_color = cfg.get_padding_color(pos).clone(); let alignh = *cfg.get_alignment_horizontal(pos); let alignv = *cfg.get_alignment_vertical(pos); let justification = ( cfg.get_justification(pos), cfg.get_justification_color(pos).cloned(), ); let (count_lines, skip) = if fmt.vertical_trim { let (len, top, _) = count_empty_lines(text.as_ref()); (len, top) } else { (count_lines(text.as_ref()), 0) }; let indent_top = top_indent(&pad, alignv, count_lines, height); let mut indent_left = None; if !fmt.allow_lines_alignment { let text_width = text_width(text.as_ref(), fmt.horizontal_trim); let available = width - pad.left.size - pad.right.size; indent_left = Some(calculate_indent(alignh, text_width, available).0); } let mut lines = LinesIter::new(text); for _ in 0..skip { let _ = lines.lines.next(); } Self { lines, indent_left, indent_top, width, alignh, fmt, pad, pad_color, color, justification, } } } impl Cell where C: ANSIFmt, { fn display(&mut self, f: &mut F) -> fmt::Result { if self.indent_top > 0 { self.indent_top -= 1; print_padding_n(f, &self.pad.top, self.pad_color.top.as_ref(), self.width)?; return Ok(()); } let line = match self.lines.lines.next() { Some(line) => line, None => { let color = self.pad_color.bottom.as_ref(); print_padding_n(f, &self.pad.bottom, color, self.width)?; return Ok(()); } }; let line = if self.fmt.horizontal_trim && !line.is_empty() { string_trim(&line) } else { line }; let line_width = get_line_width(&line); let available_width = self.width - self.pad.left.size - self.pad.right.size; let (left, right) = if self.fmt.allow_lines_alignment { calculate_indent(self.alignh, line_width, available_width) } else { let left = self.indent_left.expect("must be here"); (left, available_width - line_width - left) }; let (justification, justification_color) = (self.justification.0, self.justification.1.as_ref()); print_padding(f, &self.pad.left, self.pad_color.left.as_ref())?; print_indent(f, justification, left, justification_color)?; print_text(f, &line, self.color.as_ref())?; print_indent(f, justification, right, justification_color)?; print_padding(f, &self.pad.right, self.pad_color.right.as_ref())?; Ok(()) } } struct LinesIter { _cell: C, /// SAFETY: IT'S NOT SAFE TO KEEP THE 'static REFERENCES AROUND AS THEY ARE NOT 'static in reality AND WILL BE DROPPED _text: &'static str, /// SAFETY: IT'S NOT SAFE TO KEEP THE 'static REFERENCES AROUND AS THEY ARE NOT 'static in reality AND WILL BE DROPPED lines: Lines<'static>, } impl LinesIter { fn new(cell: C) -> Self where C: AsRef, { // We want to not allocate a String/Vec. // It's currently not possible due to a lifetime issues. (It's known as self-referential struct) // // Here we change the lifetime of text. // // # Safety // // It must be safe because the referenced string and the references are dropped at the same time. // And the referenced String is guaranteed to not be changed. let text = cell.as_ref(); let text = unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(text.as_ptr(), text.len())) }; let lines = get_lines(text); Self { _cell: cell, _text: text, lines, } } } fn print_text(f: &mut F, text: &str, clr: Option) -> fmt::Result { match clr { Some(color) => { color.fmt_ansi_prefix(f)?; f.write_str(text)?; color.fmt_ansi_suffix(f) } None => f.write_str(text), } } fn prepare_coloring<'a, F: Write>( f: &mut F, clr: Option<&'a ANSIBuf>, used_color: &mut Option<&'a ANSIBuf>, ) -> fmt::Result { match clr { Some(clr) => match used_color.as_mut() { Some(used_clr) => { if **used_clr != *clr { used_clr.fmt_ansi_suffix(f)?; clr.fmt_ansi_prefix(f)?; *used_clr = clr; } } None => { clr.fmt_ansi_prefix(f)?; *used_color = Some(clr); } }, None => { if let Some(clr) = used_color.take() { clr.fmt_ansi_suffix(f)? } } } Ok(()) } fn top_indent( padding: &Sides, alignment: AlignmentVertical, cell_height: usize, available: usize, ) -> usize { let height = available - padding.top.size; let indent = indent_from_top(alignment, height, cell_height); indent + padding.top.size } fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize { match alignment { AlignmentVertical::Top => 0, AlignmentVertical::Bottom => available - real, AlignmentVertical::Center => (available - real) / 2, } } fn calculate_indent( alignment: AlignmentHorizontal, text_width: usize, available: usize, ) -> (usize, usize) { let diff = available - text_width; match alignment { AlignmentHorizontal::Left => (0, diff), AlignmentHorizontal::Right => (diff, 0), AlignmentHorizontal::Center => { let left = diff / 2; let rest = diff - left; (left, rest) } } } fn repeat_char(f: &mut F, c: char, n: usize) -> fmt::Result { for _ in 0..n { f.write_char(c)?; } Ok(()) } fn print_vertical_char( f: &mut F, cfg: &SpannedConfig, pos: Position, line: usize, count_lines: usize, count_columns: usize, ) -> fmt::Result { let symbol = match cfg.get_vertical(pos, count_columns) { Some(c) => c, None => return Ok(()), }; let symbol = cfg .lookup_vertical_char(pos, line, count_lines) .unwrap_or(symbol); let color = cfg .get_vertical_color(pos, count_columns) .or_else(|| cfg.lookup_vertical_color(pos, line, count_lines)); match color { Some(clr) => { clr.fmt_ansi_prefix(f)?; f.write_char(symbol)?; clr.fmt_ansi_suffix(f)?; } None => f.write_char(symbol)?, } Ok(()) } fn print_margin_top(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result { let indent = cfg.get_margin().top; let offset = cfg.get_margin_offset().top; let color = cfg.get_margin_color(); let color = color.top.as_ref(); print_indent_lines(f, &indent, &offset, color, width) } fn print_margin_bottom(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result { let indent = cfg.get_margin().bottom; let offset = cfg.get_margin_offset().bottom; let color = cfg.get_margin_color(); let color = color.bottom.as_ref(); print_indent_lines(f, &indent, &offset, color, width) } fn print_margin_left( f: &mut F, cfg: &SpannedConfig, line: usize, height: Option, ) -> fmt::Result { let indent = cfg.get_margin().left; let offset = cfg.get_margin_offset().left; let color = cfg.get_margin_color(); let color = color.left.as_ref(); print_margin_vertical(f, indent, offset, color, line, height) } fn print_margin_right( f: &mut F, cfg: &SpannedConfig, line: usize, height: Option, ) -> fmt::Result { let indent = cfg.get_margin().right; let offset = cfg.get_margin_offset().right; let color = cfg.get_margin_color(); let color = color.right.as_ref(); print_margin_vertical(f, indent, offset, color, line, height) } fn print_margin_vertical( f: &mut F, indent: Indent, offset: Offset, color: Option<&ANSIBuf>, line: usize, height: Option, ) -> fmt::Result { if indent.size == 0 { return Ok(()); } match offset { Offset::Begin(mut offset) => { if let Some(max) = height { offset = cmp::min(offset, max); } if line >= offset { print_indent(f, indent.fill, indent.size, color)?; } else { repeat_char(f, ' ', indent.size)?; } } Offset::End(mut offset) => { if let Some(max) = height { offset = cmp::min(offset, max); let pos = max - offset; if line >= pos { repeat_char(f, ' ', indent.size)?; } else { print_indent(f, indent.fill, indent.size, color)?; } } else { print_indent(f, indent.fill, indent.size, color)?; } } } Ok(()) } fn print_indent_lines( f: &mut F, indent: &Indent, offset: &Offset, color: Option<&ANSIBuf>, width: usize, ) -> fmt::Result { if indent.size == 0 { return Ok(()); } let (start_offset, end_offset) = match offset { Offset::Begin(start) => (*start, 0), Offset::End(end) => (0, *end), }; let start_offset = std::cmp::min(start_offset, width); let end_offset = std::cmp::min(end_offset, width); let indent_size = width - start_offset - end_offset; for i in 0..indent.size { if start_offset > 0 { repeat_char(f, ' ', start_offset)?; } if indent_size > 0 { print_indent(f, indent.fill, indent_size, color)?; } if end_offset > 0 { repeat_char(f, ' ', end_offset)?; } if i + 1 != indent.size { f.write_char('\n')?; } } Ok(()) } fn print_padding(f: &mut F, pad: &Indent, color: Option<&ANSIBuf>) -> fmt::Result { print_indent(f, pad.fill, pad.size, color) } fn print_padding_n( f: &mut F, pad: &Indent, color: Option<&ANSIBuf>, n: usize, ) -> fmt::Result { print_indent(f, pad.fill, n, color) } fn print_indent(f: &mut F, c: char, n: usize, color: Option<&ANSIBuf>) -> fmt::Result { if n == 0 { return Ok(()); } match color { Some(color) => { color.fmt_ansi_prefix(f)?; repeat_char(f, c, n)?; color.fmt_ansi_suffix(f) } None => repeat_char(f, c, n), } } fn range_width(cfg: &SpannedConfig, dims: D, start: usize, end: usize, max: usize) -> usize where D: Dimension, { let count_borders = count_verticals_in_range(cfg, start, end, max); let range_width = (start..end).map(|col| dims.get_width(col)).sum::(); count_borders + range_width } fn range_height(cfg: &SpannedConfig, dims: D, from: usize, end: usize, max: usize) -> usize where D: Dimension, { let count_borders = count_horizontals_in_range(cfg, from, end, max); let range_width = (from..end).map(|col| dims.get_height(col)).sum::(); count_borders + range_width } fn count_horizontals_in_range(cfg: &SpannedConfig, from: usize, end: usize, max: usize) -> usize { (from + 1..end) .map(|i| cfg.has_horizontal(i, max) as usize) .sum() } fn count_verticals_in_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize { (start..end) .skip(1) .map(|i| cfg.has_vertical(i, max) as usize) .sum() } fn closest_visible_row(cfg: &SpannedConfig, mut pos: Position) -> Option { loop { if cfg.is_cell_visible(pos) { return Some(pos.row()); } if pos.row() == 0 { return None; } pos -= (1, 0); } } fn convert_count_rows(row: usize, is_last: bool) -> usize { if is_last { row + 1 } else { row + 2 } } /// Trims a string. fn string_trim(text: &str) -> Cow<'_, str> { #[cfg(feature = "ansi")] { ansi_str::AnsiStr::ansi_trim(text) } #[cfg(not(feature = "ansi"))] { text.trim().into() } } fn total_width(cfg: &SpannedConfig, dimension: &D, count_columns: usize) -> usize { (0..count_columns) .map(|i| dimension.get_width(i)) .sum::() + cfg.count_vertical(count_columns) } fn total_height(cfg: &SpannedConfig, dimension: &D, count_rows: usize) -> usize { (0..count_rows) .map(|i| dimension.get_height(i)) .sum::() + cfg.count_horizontal(count_rows) } fn count_empty_lines(cell: &str) -> (usize, usize, usize) { let mut len = 0; let mut top = 0; let mut bottom = 0; let mut top_check = true; for line in get_lines(cell) { let is_empty = line.trim().is_empty(); if top_check { if is_empty { top += 1; } else { len = 1; top_check = false; } continue; } if is_empty { bottom += 1; } else { len += bottom + 1; bottom = 0; } } (len, top, bottom) } fn text_width(text: &str, trim: bool) -> usize { if trim { get_lines(text) .map(|line| get_line_width(line.trim())) .max() .unwrap_or(0) } else { get_text_width(text) } } #[cfg(test)] mod tests { use super::*; #[test] fn vertical_alignment_test() { use AlignmentVertical::*; assert_eq!(indent_from_top(Bottom, 1, 1), 0); assert_eq!(indent_from_top(Top, 1, 1), 0); assert_eq!(indent_from_top(Center, 1, 1), 0); assert_eq!(indent_from_top(Bottom, 3, 1), 2); assert_eq!(indent_from_top(Top, 3, 1), 0); assert_eq!(indent_from_top(Center, 3, 1), 1); assert_eq!(indent_from_top(Center, 4, 1), 1); } #[test] fn count_empty_lines_test() { assert_eq!(count_empty_lines("\n\nsome text\n\n\n"), (1, 2, 3)); assert_eq!(count_empty_lines("\n\nsome\ntext\n\n\n"), (2, 2, 3)); assert_eq!(count_empty_lines("\n\nsome\nsome\ntext\n\n\n"), (3, 2, 3)); assert_eq!(count_empty_lines("\n\n\n\n"), (0, 5, 0)); } } papergrid-0.14.0/src/grid/mod.rs000064400000000000000000000002501046102023000145210ustar 00000000000000//! Module contains a list of backends for pretty print tables. pub mod compact; #[cfg(feature = "std")] pub mod iterable; #[cfg(feature = "std")] pub mod peekable; papergrid-0.14.0/src/grid/peekable.rs000064400000000000000000001730701046102023000155250ustar 00000000000000//! The module contains a [`PeekableGrid`] structure. use core::borrow::Borrow; use std::{ borrow::Cow, cmp, fmt::{self, Write}, }; use crate::{ ansi::{ANSIBuf, ANSIFmt}, colors::Colors, config::{ spanned::{Offset, SpannedConfig}, Formatting, }, config::{AlignmentHorizontal, AlignmentVertical, Indent, Position, Sides}, dimension::Dimension, records::{ExactRecords, PeekableRecords, Records}, util::string::get_line_width, }; /// Grid provides a set of methods for building a text-based table. #[derive(Debug, Clone)] pub struct PeekableGrid { records: R, config: G, dimension: D, colors: C, } impl PeekableGrid { /// The new method creates a grid instance with default styles. pub fn new(records: R, config: G, dimension: D, colors: C) -> Self { PeekableGrid { records, config, dimension, colors, } } } impl PeekableGrid { /// Builds a table. pub fn build(self, mut f: F) -> fmt::Result where R: Records + PeekableRecords + ExactRecords, D: Dimension, C: Colors, G: Borrow, F: Write, { if self.records.count_columns() == 0 || self.records.hint_count_rows() == Some(0) { return Ok(()); } let ctx = PrintCtx { cfg: self.config.borrow(), colors: &self.colors, dims: &self.dimension, records: &self.records, }; print_grid(&mut f, ctx) } /// Builds a table into string. /// /// Notice that it consumes self. #[allow(clippy::inherent_to_string)] pub fn to_string(self) -> String where R: Records + PeekableRecords + ExactRecords, D: Dimension, G: Borrow, C: Colors, { let mut buf = String::new(); self.build(&mut buf).expect("It's guaranteed to never happen otherwise it's considered an stdlib error or impl error"); buf } } #[derive(Debug, Copy, Clone)] struct PrintCtx<'a, R, D, C> { records: &'a R, cfg: &'a SpannedConfig, dims: &'a D, colors: &'a C, } fn print_grid(f: &mut F, ctx: PrintCtx<'_, R, D, C>) -> fmt::Result where F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, C: Colors, { let has_spans = ctx.cfg.has_column_spans() || ctx.cfg.has_row_spans(); if has_spans { return grid_spanned::build_grid(f, ctx); } let is_basic = !ctx.cfg.has_border_colors() && !ctx.cfg.has_padding_color() && !ctx.cfg.has_justification() && !ctx.cfg.has_offset_chars() && !has_margin(ctx.cfg) && ctx.colors.is_empty(); if is_basic { grid_basic::build_grid(f, ctx) } else { grid_not_spanned::build_grid(f, ctx) } } fn has_margin(cfg: &SpannedConfig) -> bool { let margin = cfg.get_margin(); margin.left.size > 0 || margin.right.size > 0 || margin.top.size > 0 || margin.bottom.size > 0 } mod grid_basic { use super::*; struct TextCfg { alignment: AlignmentHorizontal, formatting: Formatting, justification: char, } #[derive(Debug, Clone, Copy)] struct Shape { count_rows: usize, count_columns: usize, } struct HIndent { left: usize, right: usize, } pub(super) fn build_grid(f: &mut F, ctx: PrintCtx<'_, R, D, C>) -> fmt::Result where F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, { let shape = Shape { count_rows: ctx.records.count_rows(), count_columns: ctx.records.count_columns(), }; let mut new_line = false; for row in 0..shape.count_rows { let height = ctx.dims.get_height(row); let has_horizontal = ctx.cfg.has_horizontal(row, shape.count_rows); if new_line && (has_horizontal || height > 0) { f.write_char('\n')?; new_line = false; } if has_horizontal { print_split_line(f, ctx.cfg, ctx.dims, row, shape)?; if height > 0 { f.write_char('\n')?; } else { new_line = true; } } if height > 0 { print_grid_line(f, &ctx, shape, height, row, 0)?; for i in 1..height { f.write_char('\n')?; print_grid_line(f, &ctx, shape, height, row, i)?; } new_line = true; } } if ctx.cfg.has_horizontal(shape.count_rows, shape.count_rows) { f.write_char('\n')?; print_split_line(f, ctx.cfg, ctx.dims, shape.count_rows, shape)?; } Ok(()) } fn print_grid_line( f: &mut F, ctx: &PrintCtx<'_, R, D, C>, shape: Shape, height: usize, row: usize, line: usize, ) -> fmt::Result where F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, { for col in 0..shape.count_columns { let pos = (row, col).into(); print_vertical_char(f, ctx.cfg, pos, shape.count_columns)?; print_cell_line(f, ctx, height, pos, line)?; } let pos = (row, shape.count_columns).into(); print_vertical_char(f, ctx.cfg, pos, shape.count_columns)?; Ok(()) } fn print_split_line( f: &mut F, cfg: &SpannedConfig, dimension: &D, row: usize, shape: Shape, ) -> fmt::Result where F: Write, D: Dimension, { print_vertical_intersection(f, cfg, (row, 0).into(), shape)?; for col in 0..shape.count_columns { let width = dimension.get_width(col); // general case if width > 0 { let pos = (row, col).into(); let main = cfg.get_horizontal(pos, shape.count_rows); match main { Some(c) => repeat_char(f, c, width)?, None => repeat_char(f, ' ', width)?, } } let pos = (row, col + 1).into(); print_vertical_intersection(f, cfg, pos, shape)?; } Ok(()) } fn print_vertical_intersection( f: &mut F, cfg: &SpannedConfig, pos: Position, shape: Shape, ) -> fmt::Result where F: fmt::Write, { let intersection = cfg.get_intersection(pos, (shape.count_rows, shape.count_columns)); let intersection = match intersection { Some(c) => c, None => return Ok(()), }; // We need to make sure that we need to print it. // Specifically for cases where we have a limited amount of verticals. // // todo: Yes... this check very likely degrages performance a bit, // Surely we need to rethink it. if !cfg.has_vertical(pos.col(), shape.count_columns) { return Ok(()); } f.write_char(intersection)?; Ok(()) } fn print_vertical_char( f: &mut F, cfg: &SpannedConfig, pos: Position, count_columns: usize, ) -> fmt::Result where F: Write, { let symbol = match cfg.get_vertical(pos, count_columns) { Some(c) => c, None => return Ok(()), }; f.write_char(symbol)?; Ok(()) } fn print_cell_line( f: &mut F, ctx: &PrintCtx<'_, R, D, C>, height: usize, pos: Position, line: usize, ) -> fmt::Result where F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, { let width = ctx.dims.get_width(pos.col()); let pad = ctx.cfg.get_padding(pos); let valignment = *ctx.cfg.get_alignment_vertical(pos); let text_cfg = TextCfg { alignment: *ctx.cfg.get_alignment_horizontal(pos), formatting: ctx.cfg.get_formatting(pos), justification: ctx.cfg.get_justification(pos), }; let mut cell_height = ctx.records.count_lines(pos); if text_cfg.formatting.vertical_trim { cell_height -= count_empty_lines_at_start(ctx.records, pos) + count_empty_lines_at_end(ctx.records, pos); } if cell_height > height { // it may happen if the height estimation decide so cell_height = height; } let indent = top_indent(&pad, valignment, cell_height, height); if indent > line { return repeat_char(f, pad.top.fill, width); } let mut index = line - indent; let cell_has_this_line = cell_height > index; if !cell_has_this_line { // happens when other cells have bigger height return repeat_char(f, pad.bottom.fill, width); } if text_cfg.formatting.vertical_trim { let empty_lines = count_empty_lines_at_start(ctx.records, pos); index += empty_lines; if index > ctx.records.count_lines(pos) { return repeat_char(f, pad.top.fill, width); } } let width = width - pad.left.size - pad.right.size; repeat_char(f, pad.left.fill, pad.left.size)?; print_line(f, ctx.records, pos, index, width, text_cfg)?; repeat_char(f, pad.right.fill, pad.right.size)?; Ok(()) } fn print_line( f: &mut F, records: &R, pos: Position, index: usize, available: usize, cfg: TextCfg, ) -> fmt::Result where F: Write, R: Records + PeekableRecords, { let line = records.get_line(pos, index); let (line, line_width) = if cfg.formatting.horizontal_trim { let line = string_trim(line); let width = get_line_width(&line); (line, width) } else { let width = records.get_line_width(pos, index); (Cow::Borrowed(line), width) }; if cfg.formatting.allow_lines_alignment { let indent = calculate_indent(cfg.alignment, line_width, available); return print_text_padded(f, &line, cfg.justification, indent); } let cell_width = if cfg.formatting.horizontal_trim { (0..records.count_lines(pos)) .map(|i| records.get_line(pos, i)) .map(|line| get_line_width(line.trim())) .max() .unwrap_or_default() } else { records.get_width(pos) }; let indent = calculate_indent(cfg.alignment, cell_width, available); print_text_padded(f, &line, cfg.justification, indent)?; // todo: remove me? let rest_width = cell_width - line_width; repeat_char(f, ' ', rest_width)?; Ok(()) } fn print_text_padded(f: &mut F, text: &str, space: char, indent: HIndent) -> fmt::Result where F: Write, { repeat_char(f, space, indent.left)?; f.write_str(text)?; repeat_char(f, space, indent.right)?; Ok(()) } fn top_indent( pad: &Sides, alignment: AlignmentVertical, height: usize, available: usize, ) -> usize { let available = available - pad.top.size; let indent = indent_from_top(alignment, available, height); indent + pad.top.size } fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize { match alignment { AlignmentVertical::Top => 0, AlignmentVertical::Bottom => available - real, AlignmentVertical::Center => (available - real) / 2, } } fn calculate_indent(alignment: AlignmentHorizontal, width: usize, available: usize) -> HIndent { let diff = available - width; let (left, right) = match alignment { AlignmentHorizontal::Left => (0, diff), AlignmentHorizontal::Right => (diff, 0), AlignmentHorizontal::Center => { let left = diff / 2; let rest = diff - left; (left, rest) } }; HIndent { left, right } } fn repeat_char(f: &mut F, c: char, n: usize) -> fmt::Result where F: Write, { for _ in 0..n { f.write_char(c)?; } Ok(()) } fn count_empty_lines_at_end(records: &R, pos: Position) -> usize where R: Records + PeekableRecords, { (0..records.count_lines(pos)) .map(|i| records.get_line(pos, i)) .rev() .take_while(|l| l.trim().is_empty()) .count() } fn count_empty_lines_at_start(records: &R, pos: Position) -> usize where R: Records + PeekableRecords, { (0..records.count_lines(pos)) .map(|i| records.get_line(pos, i)) .take_while(|s| s.trim().is_empty()) .count() } /// Trims a string. fn string_trim(text: &str) -> Cow<'_, str> { #[cfg(feature = "ansi")] { ansi_str::AnsiStr::ansi_trim(text) } #[cfg(not(feature = "ansi"))] { text.trim().into() } } } mod grid_not_spanned { use super::*; struct TextCfg { alignment: AlignmentHorizontal, formatting: Formatting, color: Option, justification: Colored, } struct Colored { data: T, color: Option, } impl Colored { fn new(data: T, color: Option) -> Self { Self { data, color } } } #[derive(Debug, Clone, Copy)] struct Shape { count_rows: usize, count_columns: usize, } struct HIndent { left: usize, right: usize, } pub(super) fn build_grid(f: &mut F, ctx: PrintCtx<'_, R, D, C>) -> fmt::Result where F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, C: Colors, { let shape = Shape { count_rows: ctx.records.count_rows(), count_columns: ctx.records.count_columns(), }; let total_width = total_width(ctx.cfg, ctx.dims, shape.count_columns); let margin = ctx.cfg.get_margin(); let total_width_with_margin = total_width + margin.left.size + margin.right.size; let total_height = total_height(ctx.cfg, ctx.dims, shape.count_rows); if margin.top.size > 0 { print_margin_top(f, ctx.cfg, total_width_with_margin)?; f.write_char('\n')?; } let mut table_line = 0; let mut prev_empty_horizontal = false; for row in 0..shape.count_rows { let height = ctx.dims.get_height(row); if ctx.cfg.has_horizontal(row, shape.count_rows) { if prev_empty_horizontal { f.write_char('\n')?; } print_margin_left(f, ctx.cfg, table_line, total_height)?; print_split_line(f, ctx.cfg, ctx.dims, row, shape)?; print_margin_right(f, ctx.cfg, table_line, total_height)?; if height > 0 { f.write_char('\n')?; prev_empty_horizontal = false; } else { prev_empty_horizontal = true; } table_line += 1; } else if height > 0 && prev_empty_horizontal { f.write_char('\n')?; prev_empty_horizontal = false; } for i in 0..height { print_margin_left(f, ctx.cfg, table_line, total_height)?; for col in 0..shape.count_columns { let pos = (row, col).into(); print_vertical_char(f, ctx.cfg, pos, i, height, shape.count_columns)?; print_cell_line(f, &ctx, height, pos, i)?; let is_last_column = col + 1 == shape.count_columns; if is_last_column { let pos = (row, col + 1).into(); print_vertical_char(f, ctx.cfg, pos, i, height, shape.count_columns)?; } } print_margin_right(f, ctx.cfg, table_line, total_height)?; let is_last_line = i + 1 == height; let is_last_row = row + 1 == shape.count_rows; if !(is_last_line && is_last_row) { f.write_char('\n')?; } table_line += 1; } } if ctx.cfg.has_horizontal(shape.count_rows, shape.count_rows) { f.write_char('\n')?; print_margin_left(f, ctx.cfg, table_line, total_height)?; print_split_line(f, ctx.cfg, ctx.dims, shape.count_rows, shape)?; print_margin_right(f, ctx.cfg, table_line, total_height)?; } if margin.bottom.size > 0 { f.write_char('\n')?; print_margin_bottom(f, ctx.cfg, total_width_with_margin)?; } Ok(()) } fn print_split_line( f: &mut F, cfg: &SpannedConfig, dimension: &D, row: usize, shape: Shape, ) -> fmt::Result where F: Write, D: Dimension, { let mut used_color = None; print_vertical_intersection(f, cfg, (row, 0).into(), shape, &mut used_color)?; for col in 0..shape.count_columns { let width = dimension.get_width(col); // general case if width > 0 { let pos = (row, col).into(); let main = cfg.get_horizontal(pos, shape.count_rows); match main { Some(c) => { let clr = cfg.get_horizontal_color(pos, shape.count_rows); prepare_coloring(f, clr, &mut used_color)?; print_horizontal_border(f, cfg, pos, width, c, &used_color)?; } None => repeat_char(f, ' ', width)?, } } let pos = (row, col + 1).into(); print_vertical_intersection(f, cfg, pos, shape, &mut used_color)?; } if let Some(clr) = used_color.take() { clr.fmt_ansi_suffix(f)?; } Ok(()) } fn print_vertical_intersection<'a, F>( f: &mut F, cfg: &'a SpannedConfig, pos: Position, shape: Shape, used_color: &mut Option<&'a ANSIBuf>, ) -> fmt::Result where F: fmt::Write, { let intersection = match cfg.get_intersection(pos, (shape.count_rows, shape.count_columns)) { Some(c) => c, None => return Ok(()), }; // We need to make sure that we need to print it. // Specifically for cases where we have a limited amount of verticals. // // todo: Yes... this check very likely degrages performance a bit, // Surely we need to rethink it. if !cfg.has_vertical(pos.col(), shape.count_columns) { return Ok(()); } let color = cfg.get_intersection_color(pos, (shape.count_rows, shape.count_columns)); prepare_coloring(f, color, used_color)?; f.write_char(intersection)?; Ok(()) } fn prepare_coloring<'a, F>( f: &mut F, clr: Option<&'a ANSIBuf>, used_color: &mut Option<&'a ANSIBuf>, ) -> fmt::Result where F: Write, { match clr { Some(clr) => match used_color.as_mut() { Some(used_clr) => { if **used_clr != *clr { used_clr.fmt_ansi_suffix(f)?; clr.fmt_ansi_prefix(f)?; *used_clr = clr; } } None => { clr.fmt_ansi_prefix(f)?; *used_color = Some(clr); } }, None => { if let Some(clr) = used_color.take() { clr.fmt_ansi_suffix(f)? } } } Ok(()) } fn print_vertical_char( f: &mut F, cfg: &SpannedConfig, pos: Position, line: usize, count_lines: usize, count_columns: usize, ) -> fmt::Result where F: Write, { let symbol = match cfg.get_vertical(pos, count_columns) { Some(c) => c, None => return Ok(()), }; let symbol = cfg .lookup_vertical_char(pos, line, count_lines) .unwrap_or(symbol); let color = cfg .get_vertical_color(pos, count_columns) .or_else(|| cfg.lookup_vertical_color(pos, line, count_lines)); match color { Some(clr) => { clr.fmt_ansi_prefix(f)?; f.write_char(symbol)?; clr.fmt_ansi_suffix(f)?; } None => f.write_char(symbol)?, } Ok(()) } fn print_horizontal_border( f: &mut F, cfg: &SpannedConfig, pos: Position, width: usize, c: char, used_color: &Option<&ANSIBuf>, ) -> fmt::Result where F: Write, { if !cfg.is_overridden_horizontal(pos) { return repeat_char(f, c, width); } for i in 0..width { let c = cfg.lookup_horizontal_char(pos, i, width).unwrap_or(c); match cfg.lookup_horizontal_color(pos, i, width) { Some(color) => match used_color { Some(clr) => { clr.fmt_ansi_suffix(f)?; color.fmt_ansi_prefix(f)?; f.write_char(c)?; color.fmt_ansi_suffix(f)?; clr.fmt_ansi_prefix(f)?; } None => { color.fmt_ansi_prefix(f)?; f.write_char(c)?; color.fmt_ansi_suffix(f)?; } }, _ => f.write_char(c)?, } } Ok(()) } fn print_cell_line( f: &mut F, ctx: &PrintCtx<'_, R, D, C>, height: usize, pos: Position, line: usize, ) -> fmt::Result where F: Write, R: Records + PeekableRecords + ExactRecords, C: Colors, D: Dimension, { let width = ctx.dims.get_width(pos.col()); let formatting = ctx.cfg.get_formatting(pos); let text_cfg = TextCfg { alignment: *ctx.cfg.get_alignment_horizontal(pos), color: ctx.colors.get_color(pos), justification: Colored::new( ctx.cfg.get_justification(pos), ctx.cfg.get_justification_color(pos), ), formatting, }; let pad = ctx.cfg.get_padding(pos); let pad_color = ctx.cfg.get_padding_color(pos); let valignment = *ctx.cfg.get_alignment_vertical(pos); let mut cell_height = ctx.records.count_lines(pos); if formatting.vertical_trim { cell_height -= count_empty_lines_at_start(ctx.records, pos) + count_empty_lines_at_end(ctx.records, pos); } if cell_height > height { // it may happen if the height estimation decide so cell_height = height; } let indent = top_indent(&pad, valignment, cell_height, height); if indent > line { return print_indent(f, pad.top.fill, width, pad_color.top.as_ref()); } let mut index = line - indent; let cell_has_this_line = cell_height > index; if !cell_has_this_line { // happens when other cells have bigger height return print_indent(f, pad.bottom.fill, width, pad_color.bottom.as_ref()); } if formatting.vertical_trim { let empty_lines = count_empty_lines_at_start(ctx.records, pos); index += empty_lines; if index > ctx.records.count_lines(pos) { return print_indent(f, pad.top.fill, width, pad_color.top.as_ref()); } } let width = width - pad.left.size - pad.right.size; print_indent(f, pad.left.fill, pad.left.size, pad_color.left.as_ref())?; print_line(f, ctx.records, pos, index, width, text_cfg)?; print_indent(f, pad.right.fill, pad.right.size, pad_color.right.as_ref())?; Ok(()) } fn print_line( f: &mut F, records: &R, pos: Position, index: usize, available: usize, cfg: TextCfg, ) -> fmt::Result where F: Write, R: Records + PeekableRecords, C: ANSIFmt, { let line = records.get_line(pos, index); let (line, line_width) = if cfg.formatting.horizontal_trim { let line = string_trim(line); let width = get_line_width(&line); (line, width) } else { let width = records.get_line_width(pos, index); (Cow::Borrowed(line), width) }; if cfg.formatting.allow_lines_alignment { let indent = calculate_indent(cfg.alignment, line_width, available); let text = Colored::new(line.as_ref(), cfg.color); return print_text_padded(f, text, cfg.justification, indent); } let cell_width = if cfg.formatting.horizontal_trim { (0..records.count_lines(pos)) .map(|i| records.get_line(pos, i)) .map(|line| get_line_width(line.trim())) .max() .unwrap_or_default() } else { records.get_width(pos) }; let indent = calculate_indent(cfg.alignment, cell_width, available); let text = Colored::new(line.as_ref(), cfg.color); print_text_padded(f, text, cfg.justification, indent)?; // todo: remove me? let rest_width = cell_width - line_width; repeat_char(f, ' ', rest_width)?; Ok(()) } fn print_text_padded( f: &mut F, text: Colored<&str, C>, justification: Colored, indent: HIndent, ) -> fmt::Result where F: Write, C: ANSIFmt, C1: ANSIFmt, { print_indent2(f, &justification, indent.left)?; print_text2(f, text)?; print_indent2(f, &justification, indent.right)?; Ok(()) } fn print_text2(f: &mut F, text: Colored<&str, C>) -> fmt::Result where F: Write, C: ANSIFmt, { match text.color { Some(color) => { color.fmt_ansi_prefix(f)?; f.write_str(text.data)?; color.fmt_ansi_suffix(f) } None => f.write_str(text.data), } } fn top_indent( pad: &Sides, alignment: AlignmentVertical, height: usize, available: usize, ) -> usize { let available = available - pad.top.size; let indent = indent_from_top(alignment, available, height); indent + pad.top.size } fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize { match alignment { AlignmentVertical::Top => 0, AlignmentVertical::Bottom => available - real, AlignmentVertical::Center => (available - real) / 2, } } fn calculate_indent(alignment: AlignmentHorizontal, width: usize, available: usize) -> HIndent { let diff = available - width; let (left, right) = match alignment { AlignmentHorizontal::Left => (0, diff), AlignmentHorizontal::Right => (diff, 0), AlignmentHorizontal::Center => { let left = diff / 2; let rest = diff - left; (left, rest) } }; HIndent { left, right } } fn repeat_char(f: &mut F, c: char, n: usize) -> fmt::Result where F: Write, { for _ in 0..n { f.write_char(c)?; } Ok(()) } fn count_empty_lines_at_end(records: &R, pos: Position) -> usize where R: Records + PeekableRecords, { (0..records.count_lines(pos)) .map(|i| records.get_line(pos, i)) .rev() .take_while(|l| l.trim().is_empty()) .count() } fn count_empty_lines_at_start(records: &R, pos: Position) -> usize where R: Records + PeekableRecords, { (0..records.count_lines(pos)) .map(|i| records.get_line(pos, i)) .take_while(|s| s.trim().is_empty()) .count() } fn total_width(cfg: &SpannedConfig, dimension: &D, count_columns: usize) -> usize where D: Dimension, { (0..count_columns) .map(|i| dimension.get_width(i)) .sum::() + cfg.count_vertical(count_columns) } fn total_height(cfg: &SpannedConfig, dimension: &D, count_rows: usize) -> usize where D: Dimension, { (0..count_rows) .map(|i| dimension.get_height(i)) .sum::() + cfg.count_horizontal(count_rows) } fn print_margin_top(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result where F: Write, { let indent = cfg.get_margin().top; let offset = cfg.get_margin_offset().top; let color = cfg.get_margin_color(); let color = color.top.as_ref(); print_indent_lines(f, indent, offset, color, width) } fn print_margin_bottom(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result where F: Write, { let indent = cfg.get_margin().bottom; let offset = cfg.get_margin_offset().bottom; let color = cfg.get_margin_color(); let color = color.bottom.as_ref(); print_indent_lines(f, indent, offset, color, width) } fn print_margin_left( f: &mut F, cfg: &SpannedConfig, line: usize, height: usize, ) -> fmt::Result where F: Write, { let indent = cfg.get_margin().left; let offset = cfg.get_margin_offset().left; let color = cfg.get_margin_color(); let color = color.left.as_ref(); print_margin_vertical(f, indent, offset, color, line, height) } fn print_margin_right( f: &mut F, cfg: &SpannedConfig, line: usize, height: usize, ) -> fmt::Result where F: Write, { let indent = cfg.get_margin().right; let offset = cfg.get_margin_offset().right; let color = cfg.get_margin_color(); let color = color.right.as_ref(); print_margin_vertical(f, indent, offset, color, line, height) } fn print_margin_vertical( f: &mut F, indent: Indent, offset: Offset, color: Option<&ANSIBuf>, line: usize, height: usize, ) -> fmt::Result where F: Write, { if indent.size == 0 { return Ok(()); } match offset { Offset::Begin(offset) => { let offset = cmp::min(offset, height); if line >= offset { print_indent(f, indent.fill, indent.size, color)?; } else { repeat_char(f, ' ', indent.size)?; } } Offset::End(offset) => { let offset = cmp::min(offset, height); let pos = height - offset; if line >= pos { repeat_char(f, ' ', indent.size)?; } else { print_indent(f, indent.fill, indent.size, color)?; } } } Ok(()) } fn print_indent_lines( f: &mut F, indent: Indent, offset: Offset, color: Option<&ANSIBuf>, width: usize, ) -> fmt::Result where F: Write, { if indent.size == 0 { return Ok(()); } let (start_offset, end_offset) = match offset { Offset::Begin(start) => (start, 0), Offset::End(end) => (0, end), }; let start_offset = std::cmp::min(start_offset, width); let end_offset = std::cmp::min(end_offset, width); let indent_size = width - start_offset - end_offset; for i in 0..indent.size { if start_offset > 0 { repeat_char(f, ' ', start_offset)?; } if indent_size > 0 { print_indent(f, indent.fill, indent_size, color)?; } if end_offset > 0 { repeat_char(f, ' ', end_offset)?; } if i + 1 != indent.size { f.write_char('\n')?; } } Ok(()) } fn print_indent(f: &mut F, c: char, n: usize, color: Option) -> fmt::Result where F: Write, C: ANSIFmt, { if n == 0 { return Ok(()); } match color { Some(color) => { color.fmt_ansi_prefix(f)?; repeat_char(f, c, n)?; color.fmt_ansi_suffix(f) } None => repeat_char(f, c, n), } } fn print_indent2(f: &mut F, c: &Colored, n: usize) -> fmt::Result where F: Write, C: ANSIFmt, { if n == 0 { return Ok(()); } match &c.color { Some(color) => { color.fmt_ansi_prefix(f)?; repeat_char(f, c.data, n)?; color.fmt_ansi_suffix(f) } None => repeat_char(f, c.data, n), } } /// Trims a string. fn string_trim(text: &str) -> Cow<'_, str> { #[cfg(feature = "ansi")] { ansi_str::AnsiStr::ansi_trim(text) } #[cfg(not(feature = "ansi"))] { text.trim().into() } } } mod grid_spanned { use super::*; struct TextCfg { alignment: AlignmentHorizontal, formatting: Formatting, color: Option, justification: Colored, } struct Colored { data: T, color: Option, } impl Colored { fn new(data: T, color: Option) -> Self { Self { data, color } } } #[derive(Debug, Copy, Clone)] struct Shape { count_rows: usize, count_columns: usize, } struct HIndent { left: usize, right: usize, } pub(super) fn build_grid(f: &mut F, ctx: PrintCtx<'_, R, D, C>) -> fmt::Result where F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, C: Colors, { let shape = Shape { count_rows: ctx.records.count_rows(), count_columns: ctx.records.count_columns(), }; let total_width = total_width(ctx.cfg, ctx.dims, shape.count_columns); let margin = ctx.cfg.get_margin(); let total_width_with_margin = total_width + margin.left.size + margin.right.size; let total_height = total_height(ctx.cfg, ctx.dims, shape.count_rows); if margin.top.size > 0 { print_margin_top(f, ctx.cfg, total_width_with_margin)?; f.write_char('\n')?; } let mut table_line = 0; let mut prev_empty_horizontal = false; for row in 0..shape.count_rows { let count_lines = ctx.dims.get_height(row); if ctx.cfg.has_horizontal(row, shape.count_rows) { if prev_empty_horizontal { f.write_char('\n')?; } print_margin_left(f, ctx.cfg, table_line, total_height)?; print_split_line_spanned(f, &ctx, row, shape)?; print_margin_right(f, ctx.cfg, table_line, total_height)?; if count_lines > 0 { f.write_char('\n')?; prev_empty_horizontal = false; } else { prev_empty_horizontal = true; } table_line += 1; } else if count_lines > 0 && prev_empty_horizontal { f.write_char('\n')?; prev_empty_horizontal = false; } for i in 0..count_lines { print_margin_left(f, ctx.cfg, table_line, total_height)?; for col in 0..shape.count_columns { let pos = (row, col).into(); if ctx.cfg.is_cell_covered_by_both_spans(pos) { continue; } if ctx.cfg.is_cell_covered_by_column_span(pos) { let is_last_column = col + 1 == shape.count_columns; if is_last_column { let pos = (row, col + 1).into(); let count_columns = shape.count_columns; print_vertical_char(f, ctx.cfg, pos, i, count_lines, count_columns)?; } continue; } print_vertical_char(f, ctx.cfg, pos, i, count_lines, shape.count_columns)?; if ctx.cfg.is_cell_covered_by_row_span(pos) { // means it's part of other a spanned cell // so. we just need to use line from other cell. let original_row = closest_visible_row(ctx.cfg, pos).unwrap(); // considering that the content will be printed instead horizontal lines so we can skip some lines. let mut skip_lines = (original_row..row) .map(|i| ctx.dims.get_height(i)) .sum::(); skip_lines += (original_row + 1..=row) .map(|row| ctx.cfg.has_horizontal(row, shape.count_rows) as usize) .sum::(); let line = i + skip_lines; let pos = (original_row, col).into(); let width = get_cell_width(ctx.cfg, ctx.dims, pos, shape.count_columns); let height = get_cell_height(ctx.cfg, ctx.dims, pos, shape.count_rows); print_cell_line(f, &ctx, width, height, pos, line)?; } else { let width = get_cell_width(ctx.cfg, ctx.dims, pos, shape.count_columns); let height = get_cell_height(ctx.cfg, ctx.dims, pos, shape.count_rows); print_cell_line(f, &ctx, width, height, pos, i)?; } let is_last_column = col + 1 == shape.count_columns; if is_last_column { let pos = (row, col + 1).into(); print_vertical_char(f, ctx.cfg, pos, i, count_lines, shape.count_columns)?; } } print_margin_right(f, ctx.cfg, table_line, total_height)?; let is_last_line = i + 1 == count_lines; let is_last_row = row + 1 == shape.count_rows; if !(is_last_line && is_last_row) { f.write_char('\n')?; } table_line += 1; } } if ctx.cfg.has_horizontal(shape.count_rows, shape.count_rows) { f.write_char('\n')?; print_margin_left(f, ctx.cfg, table_line, total_height)?; print_split_line(f, ctx.cfg, ctx.dims, shape.count_rows, shape)?; print_margin_right(f, ctx.cfg, table_line, total_height)?; } if margin.bottom.size > 0 { f.write_char('\n')?; print_margin_bottom(f, ctx.cfg, total_width_with_margin)?; } Ok(()) } fn print_split_line_spanned( f: &mut F, ctx: &PrintCtx<'_, R, D, C>, row: usize, shape: Shape, ) -> fmt::Result where F: Write, R: Records + ExactRecords + PeekableRecords, D: Dimension, C: Colors, { let mut used_color = None; let pos = (row, 0).into(); print_vertical_intersection(f, ctx.cfg, pos, shape, &mut used_color)?; for col in 0..shape.count_columns { let pos = (row, col).into(); if ctx.cfg.is_cell_covered_by_both_spans(pos) { continue; } if ctx.cfg.is_cell_covered_by_row_span(pos) { // means it's part of other a spanned cell // so. we just need to use line from other cell. let original_row = closest_visible_row(ctx.cfg, pos).unwrap(); // considering that the content will be printed instead horizontal lines so we can skip some lines. let mut skip_lines = (original_row..row) .map(|i| ctx.dims.get_height(i)) .sum::(); // skip horizontal lines if row > 0 { skip_lines += (original_row..row - 1) .map(|row| ctx.cfg.has_horizontal(row + 1, shape.count_rows) as usize) .sum::(); } let pos = (original_row, col).into(); let height = get_cell_height(ctx.cfg, ctx.dims, pos, shape.count_rows); let width = get_cell_width(ctx.cfg, ctx.dims, pos, shape.count_columns); let line = skip_lines; print_cell_line(f, ctx, width, height, pos, line)?; // We need to use a correct right split char. let mut col = col; if let Some(span) = ctx.cfg.get_column_span(pos) { col += span - 1; } let pos = (row, col + 1).into(); print_vertical_intersection(f, ctx.cfg, pos, shape, &mut used_color)?; continue; } let width = ctx.dims.get_width(col); if width > 0 { // general case let main = ctx.cfg.get_horizontal(pos, shape.count_rows); match main { Some(c) => { let clr = ctx.cfg.get_horizontal_color(pos, shape.count_rows); prepare_coloring(f, clr, &mut used_color)?; print_horizontal_border(f, ctx.cfg, pos, width, c, &used_color)?; } None => repeat_char(f, ' ', width)?, } } let pos = (row, col + 1).into(); print_vertical_intersection(f, ctx.cfg, pos, shape, &mut used_color)?; } if let Some(clr) = used_color { clr.fmt_ansi_suffix(f)?; } Ok(()) } fn print_vertical_char( f: &mut F, cfg: &SpannedConfig, pos: Position, line: usize, count_lines: usize, count_columns: usize, ) -> fmt::Result where F: Write, { let symbol = match cfg.get_vertical(pos, count_columns) { Some(c) => c, None => return Ok(()), }; let symbol = cfg .lookup_vertical_char(pos, line, count_lines) .unwrap_or(symbol); let color = cfg .get_vertical_color(pos, count_columns) .or_else(|| cfg.lookup_vertical_color(pos, line, count_lines)); match color { Some(clr) => { clr.fmt_ansi_prefix(f)?; f.write_char(symbol)?; clr.fmt_ansi_suffix(f)?; } None => f.write_char(symbol)?, } Ok(()) } fn print_vertical_intersection<'a, F>( f: &mut F, cfg: &'a SpannedConfig, pos: Position, shape: Shape, used_color: &mut Option<&'a ANSIBuf>, ) -> fmt::Result where F: fmt::Write, { let intersection = match cfg.get_intersection(pos, (shape.count_rows, shape.count_columns)) { Some(c) => c, None => return Ok(()), }; // We need to make sure that we need to print it. // Specifically for cases where we have a limited amount of verticals. // // todo: Yes... this check very likely degrages performance a bit, // Surely we need to rethink it. if !cfg.has_vertical(pos.col(), shape.count_columns) { return Ok(()); } let color = cfg.get_intersection_color(pos, (shape.count_rows, shape.count_columns)); prepare_coloring(f, color, used_color)?; f.write_char(intersection)?; Ok(()) } fn prepare_coloring<'a, F>( f: &mut F, clr: Option<&'a ANSIBuf>, used_color: &mut Option<&'a ANSIBuf>, ) -> fmt::Result where F: Write, { match clr { Some(clr) => match used_color.as_mut() { Some(used_clr) => { if **used_clr != *clr { used_clr.fmt_ansi_suffix(f)?; clr.fmt_ansi_prefix(f)?; *used_clr = clr; } } None => { clr.fmt_ansi_prefix(f)?; *used_color = Some(clr); } }, None => { if let Some(clr) = used_color.take() { clr.fmt_ansi_suffix(f)? } } } Ok(()) } fn print_split_line( f: &mut F, cfg: &SpannedConfig, dimension: &D, row: usize, shape: Shape, ) -> fmt::Result where F: Write, D: Dimension, { let mut used_color = None; print_vertical_intersection(f, cfg, (row, 0).into(), shape, &mut used_color)?; for col in 0..shape.count_columns { let width = dimension.get_width(col); // general case if width > 0 { let pos = (row, col).into(); let main = cfg.get_horizontal(pos, shape.count_rows); match main { Some(c) => { let clr = cfg.get_horizontal_color(pos, shape.count_rows); prepare_coloring(f, clr, &mut used_color)?; print_horizontal_border(f, cfg, pos, width, c, &used_color)?; } None => repeat_char(f, ' ', width)?, } } let pos = (row, col + 1).into(); print_vertical_intersection(f, cfg, pos, shape, &mut used_color)?; } if let Some(clr) = used_color.take() { clr.fmt_ansi_suffix(f)?; } Ok(()) } fn print_horizontal_border( f: &mut F, cfg: &SpannedConfig, pos: Position, width: usize, c: char, used_color: &Option<&ANSIBuf>, ) -> fmt::Result where F: Write, { if !cfg.is_overridden_horizontal(pos) { return repeat_char(f, c, width); } for i in 0..width { let c = cfg.lookup_horizontal_char(pos, i, width).unwrap_or(c); match cfg.lookup_horizontal_color(pos, i, width) { Some(color) => match used_color { Some(clr) => { clr.fmt_ansi_suffix(f)?; color.fmt_ansi_prefix(f)?; f.write_char(c)?; color.fmt_ansi_suffix(f)?; clr.fmt_ansi_prefix(f)?; } None => { color.fmt_ansi_prefix(f)?; f.write_char(c)?; color.fmt_ansi_suffix(f)?; } }, _ => f.write_char(c)?, } } Ok(()) } fn print_cell_line( f: &mut F, ctx: &PrintCtx<'_, R, D, C>, width: usize, height: usize, pos: Position, line: usize, ) -> fmt::Result where F: Write, R: Records + PeekableRecords + ExactRecords, C: Colors, { let mut cell_height = ctx.records.count_lines(pos); let formatting = ctx.cfg.get_formatting(pos); if formatting.vertical_trim { cell_height -= count_empty_lines_at_start(ctx.records, pos) + count_empty_lines_at_end(ctx.records, pos); } if cell_height > height { // it may happen if the height estimation decide so cell_height = height; } let pad = ctx.cfg.get_padding(pos); let pad_color = ctx.cfg.get_padding_color(pos); let alignment = ctx.cfg.get_alignment_vertical(pos); let indent = top_indent(&pad, *alignment, cell_height, height); if indent > line { return print_indent(f, pad.top.fill, width, pad_color.top.as_ref()); } let mut index = line - indent; let cell_has_this_line = cell_height > index; if !cell_has_this_line { // happens when other cells have bigger height return print_indent(f, pad.bottom.fill, width, pad_color.bottom.as_ref()); } if formatting.vertical_trim { let empty_lines = count_empty_lines_at_start(ctx.records, pos); index += empty_lines; if index > ctx.records.count_lines(pos) { return print_indent(f, pad.top.fill, width, pad_color.top.as_ref()); } } print_indent(f, pad.left.fill, pad.left.size, pad_color.left.as_ref())?; let width = width - pad.left.size - pad.right.size; let line_cfg = TextCfg { alignment: *ctx.cfg.get_alignment_horizontal(pos), color: ctx.colors.get_color(pos), justification: Colored::new( ctx.cfg.get_justification(pos), ctx.cfg.get_justification_color(pos), ), formatting, }; print_line(f, ctx.records, pos, index, width, line_cfg)?; print_indent(f, pad.right.fill, pad.right.size, pad_color.right.as_ref())?; Ok(()) } fn print_line( f: &mut F, records: &R, pos: Position, index: usize, available: usize, text_cfg: TextCfg, ) -> fmt::Result where F: Write, R: Records + PeekableRecords, C: ANSIFmt, C1: ANSIFmt, { let line = records.get_line(pos, index); let (line, line_width) = if text_cfg.formatting.horizontal_trim { let line = string_trim(line); let width = get_line_width(&line); (line, width) } else { let width = records.get_line_width(pos, index); (Cow::Borrowed(line), width) }; if text_cfg.formatting.allow_lines_alignment { let indent = calculate_indent(text_cfg.alignment, line_width, available); let text = Colored::new(line.as_ref(), text_cfg.color); return print_text_with_pad(f, text, text_cfg.justification, indent); } let cell_width = if text_cfg.formatting.horizontal_trim { (0..records.count_lines(pos)) .map(|i| records.get_line(pos, i)) .map(|line| get_line_width(line.trim())) .max() .unwrap_or_default() } else { records.get_width(pos) }; let indent = calculate_indent(text_cfg.alignment, cell_width, available); let text = Colored::new(line.as_ref(), text_cfg.color); print_text_with_pad(f, text, text_cfg.justification, indent)?; // todo: remove me? let rest_width = cell_width - line_width; repeat_char(f, ' ', rest_width)?; Ok(()) } fn print_text_with_pad( f: &mut F, text: Colored<&str, C>, space: Colored, indent: HIndent, ) -> fmt::Result where F: Write, C: ANSIFmt, C1: ANSIFmt, { print_indent(f, space.data, indent.left, space.color.as_ref())?; print_text(f, text.data, text.color)?; print_indent(f, space.data, indent.right, space.color.as_ref())?; Ok(()) } fn print_text(f: &mut F, text: &str, clr: Option) -> fmt::Result where F: Write, C: ANSIFmt, { match clr { Some(color) => { color.fmt_ansi_prefix(f)?; f.write_str(text)?; color.fmt_ansi_suffix(f) } None => f.write_str(text), } } fn top_indent( pad: &Sides, alignment: AlignmentVertical, cell_height: usize, available: usize, ) -> usize { let height = available - pad.top.size; let indent = indent_from_top(alignment, height, cell_height); indent + pad.top.size } fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize { match alignment { AlignmentVertical::Top => 0, AlignmentVertical::Bottom => available - real, AlignmentVertical::Center => (available - real) / 2, } } fn calculate_indent(alignment: AlignmentHorizontal, width: usize, available: usize) -> HIndent { let diff = available - width; let (left, right) = match alignment { AlignmentHorizontal::Left => (0, diff), AlignmentHorizontal::Right => (diff, 0), AlignmentHorizontal::Center => { let left = diff / 2; let rest = diff - left; (left, rest) } }; HIndent { left, right } } fn repeat_char(f: &mut F, c: char, n: usize) -> fmt::Result where F: Write, { for _ in 0..n { f.write_char(c)?; } Ok(()) } fn count_empty_lines_at_end(records: &R, pos: Position) -> usize where R: Records + PeekableRecords, { (0..records.count_lines(pos)) .map(|i| records.get_line(pos, i)) .rev() .take_while(|l| l.trim().is_empty()) .count() } fn count_empty_lines_at_start(records: &R, pos: Position) -> usize where R: Records + PeekableRecords, { (0..records.count_lines(pos)) .map(|i| records.get_line(pos, i)) .take_while(|s| s.trim().is_empty()) .count() } fn total_width(cfg: &SpannedConfig, dimension: &D, count_columns: usize) -> usize where D: Dimension, { (0..count_columns) .map(|i| dimension.get_width(i)) .sum::() + cfg.count_vertical(count_columns) } fn total_height(cfg: &SpannedConfig, dimension: &D, count_rows: usize) -> usize where D: Dimension, { (0..count_rows) .map(|i| dimension.get_height(i)) .sum::() + cfg.count_horizontal(count_rows) } fn print_margin_top(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result where F: Write, { let indent = cfg.get_margin().top; let offset = cfg.get_margin_offset().top; let color = cfg.get_margin_color(); let color = color.top.as_ref(); print_indent_lines(f, &indent, &offset, color, width) } fn print_margin_bottom(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result where F: Write, { let indent = cfg.get_margin().bottom; let offset = cfg.get_margin_offset().bottom; let color = cfg.get_margin_color(); let color = color.bottom.as_ref(); print_indent_lines(f, &indent, &offset, color, width) } fn print_margin_left( f: &mut F, cfg: &SpannedConfig, line: usize, height: usize, ) -> fmt::Result where F: Write, { let indent = cfg.get_margin().left; let offset = cfg.get_margin_offset().left; let color = cfg.get_margin_color(); let color = color.left.as_ref(); print_margin_vertical(f, indent, offset, color, line, height) } fn print_margin_right( f: &mut F, cfg: &SpannedConfig, line: usize, height: usize, ) -> fmt::Result where F: Write, { let indent = cfg.get_margin().right; let offset = cfg.get_margin_offset().right; let color = cfg.get_margin_color(); let color = color.right.as_ref(); print_margin_vertical(f, indent, offset, color, line, height) } fn print_margin_vertical( f: &mut F, indent: Indent, offset: Offset, color: Option<&ANSIBuf>, line: usize, height: usize, ) -> fmt::Result where F: Write, { if indent.size == 0 { return Ok(()); } match offset { Offset::Begin(offset) => { let offset = cmp::min(offset, height); if line >= offset { print_indent(f, indent.fill, indent.size, color)?; } else { repeat_char(f, ' ', indent.size)?; } } Offset::End(offset) => { let offset = cmp::min(offset, height); let pos = height - offset; if line >= pos { repeat_char(f, ' ', indent.size)?; } else { print_indent(f, indent.fill, indent.size, color)?; } } } Ok(()) } fn print_indent_lines( f: &mut F, indent: &Indent, offset: &Offset, color: Option<&ANSIBuf>, width: usize, ) -> fmt::Result where F: Write, { if indent.size == 0 { return Ok(()); } let (start_offset, end_offset) = match offset { Offset::Begin(start) => (*start, 0), Offset::End(end) => (0, *end), }; let start_offset = std::cmp::min(start_offset, width); let end_offset = std::cmp::min(end_offset, width); let indent_size = width - start_offset - end_offset; for i in 0..indent.size { if start_offset > 0 { repeat_char(f, ' ', start_offset)?; } if indent_size > 0 { print_indent(f, indent.fill, indent_size, color)?; } if end_offset > 0 { repeat_char(f, ' ', end_offset)?; } if i + 1 != indent.size { f.write_char('\n')?; } } Ok(()) } fn print_indent(f: &mut F, c: char, n: usize, color: Option) -> fmt::Result where F: Write, C: ANSIFmt, { if n == 0 { return Ok(()); } match color { Some(color) => { color.fmt_ansi_prefix(f)?; repeat_char(f, c, n)?; color.fmt_ansi_suffix(f) } None => repeat_char(f, c, n), } } fn get_cell_width(cfg: &SpannedConfig, dims: &D, pos: Position, max: usize) -> usize where D: Dimension, { match cfg.get_column_span(pos) { Some(span) => { let start = pos.col(); let end = start + span; range_width(dims, start, end) + count_verticals_range(cfg, start, end, max) } None => dims.get_width(pos.col()), } } fn range_width(dims: &D, start: usize, end: usize) -> usize where D: Dimension, { (start..end).map(|col| dims.get_width(col)).sum::() } fn count_verticals_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize { (start + 1..end) .map(|i| cfg.has_vertical(i, max) as usize) .sum() } fn get_cell_height(cfg: &SpannedConfig, dims: &D, pos: Position, max: usize) -> usize where D: Dimension, { match cfg.get_row_span(pos) { Some(span) => { let start = pos.row(); let end = pos.row() + span; range_height(dims, start, end) + count_horizontals_range(cfg, start, end, max) } None => dims.get_height(pos.row()), } } fn range_height(dims: &D, start: usize, end: usize) -> usize where D: Dimension, { (start..end).map(|col| dims.get_height(col)).sum::() } fn count_horizontals_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize { (start + 1..end) .map(|i| cfg.has_horizontal(i, max) as usize) .sum() } fn closest_visible_row(cfg: &SpannedConfig, mut pos: Position) -> Option { loop { if cfg.is_cell_visible(pos) { return Some(pos.row()); } if pos.row() == 0 { return None; } pos -= (1, 0); } } /// Trims a string. fn string_trim(text: &str) -> Cow<'_, str> { #[cfg(feature = "ansi")] { ansi_str::AnsiStr::ansi_trim(text) } #[cfg(not(feature = "ansi"))] { text.trim().into() } } } papergrid-0.14.0/src/lib.rs000064400000000000000000000046061046102023000135740ustar 00000000000000#![cfg_attr(not(any(feature = "std", test)), no_std)] #![warn( rust_2018_idioms, rust_2018_compatibility, rust_2021_compatibility, missing_debug_implementations, unreachable_pub, missing_docs )] #![allow(clippy::uninlined_format_args)] #![deny(unused_must_use)] #![doc( html_logo_url = "https://raw.githubusercontent.com/zhiburt/tabled/86ac146e532ce9f7626608d7fd05072123603a2e/assets/tabled-gear.svg" )] //! Papergrid is a library for generating text-based tables. //! //! It has relatively low level API. //! If you're interested in a more friendly one take a look at [`tabled`](https://github.com/zhiburt/tabled). //! //! # Example //! #![cfg_attr(feature = "std", doc = "```")] #![cfg_attr(not(feature = "std"), doc = "```ignore")] //! use papergrid::{ //! records::IterRecords, //! dimension::{Estimate}, //! config::Borders, //! colors::NoColors, //! grid::iterable::Grid, //! config::spanned::SpannedConfig, //! dimension::spanned::SpannedGridDimension, //! }; //! //! // Creating a borders structure of a grid. //! let borders = Borders { //! top: Some('-'), //! top_left: Some('+'), //! top_right: Some('+'), //! top_intersection: Some('+'), //! bottom: Some('-'), //! bottom_left: Some('+'), //! bottom_right: Some('+'), //! bottom_intersection: Some('+'), //! horizontal: Some('-'), //! vertical: Some('|'), //! left: Some('|'), //! right: Some('|'), //! intersection: Some('+'), //! left_intersection: Some('+'), //! right_intersection: Some('+'), //! }; //! //! // Creating a grid config. //! let mut cfg = SpannedConfig::default(); //! cfg.set_borders(borders); //! //! // Creating an actual data for grid. //! let records = vec![vec!["Hello", "World"], vec!["Hi", "World"]]; //! let records = IterRecords::new(records, 2, None); //! //! // Estimate grid dimension. //! let mut dimension = SpannedGridDimension::default(); //! dimension.estimate(&records, &cfg); //! //! // Creating a grid. //! let grid = Grid::new(&records, &dimension, &cfg, NoColors).to_string(); //! //! assert_eq!( //! grid, //! concat!( //! "+-----+-----+\n", //! "|Hello|World|\n", //! "+-----+-----+\n", //! "|Hi |World|\n", //! "+-----+-----+", //! ), //! ); //! ``` pub mod ansi; pub mod colors; pub mod config; pub mod dimension; pub mod grid; pub mod records; pub mod util; papergrid-0.14.0/src/records/exact_records.rs000064400000000000000000000012101046102023000173000ustar 00000000000000/// [`Records`] extension which guarantees the amount of rows. /// /// [`Records`]: crate::records::Records pub trait ExactRecords { /// Returns an exact amount of rows in records. /// /// It must be guaranteed that an iterator will yield this amount. fn count_rows(&self) -> usize; } impl ExactRecords for &T where T: ExactRecords, { fn count_rows(&self) -> usize { T::count_rows(self) } } #[cfg(feature = "std")] impl ExactRecords for Vec { fn count_rows(&self) -> usize { self.len() } } impl ExactRecords for [T] { fn count_rows(&self) -> usize { self.len() } } papergrid-0.14.0/src/records/into_records.rs000064400000000000000000000015341046102023000171560ustar 00000000000000/// The representation of data, rows and columns of a [`Grid`]. /// /// [`Grid`]: crate::grid::iterable::Grid pub trait IntoRecords { /// A string representation of a [`Grid`] cell. /// /// [`Grid`]: crate::grid::iterable::Grid type Cell; /// Cell iterator inside a row. type IterColumns: IntoIterator; /// Rows iterator. type IterRows: IntoIterator; /// Returns an iterator over rows. fn iter_rows(self) -> Self::IterRows; } impl IntoRecords for T where T: IntoIterator, ::Item: IntoIterator, { type Cell = <::Item as IntoIterator>::Item; type IterColumns = ::Item; type IterRows = ::IntoIter; fn iter_rows(self) -> Self::IterRows { self.into_iter() } } papergrid-0.14.0/src/records/iter_records.rs000064400000000000000000000035161046102023000171520ustar 00000000000000use super::{IntoRecords, Records}; /// A [Records] implementation for any [IntoIterator]. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct IterRecords { iter: I, count_columns: usize, count_rows: Option, } impl IterRecords { /// Returns a new [IterRecords] object. pub const fn new(iter: I, count_columns: usize, count_rows: Option) -> Self { Self { iter, count_columns, count_rows, } } } impl IntoRecords for IterRecords where I: IntoRecords, { type Cell = I::Cell; type IterColumns = I::IterColumns; type IterRows = I::IterRows; fn iter_rows(self) -> Self::IterRows { self.iter.iter_rows() } } // why this does not work? // impl<'a, I> IntoRecords for &'a IterRecords // where // &'a I: IntoRecords, // { // type Cell = <&'a I as IntoRecords>::Cell; // type IterColumns = <&'a I as IntoRecords>::IterColumns; // type IterRows = <&'a I as IntoRecords>::IterRows; // fn iter_rows(self) -> Self::IterRows { // // (&self.iter).iter_rows() // todo!() // } // } impl Records for IterRecords where I: IntoRecords, { type Iter = I; fn iter_rows(self) -> ::IterRows { self.iter.iter_rows() } fn count_columns(&self) -> usize { self.count_columns } fn hint_count_rows(&self) -> Option { self.count_rows } } impl<'a, I> Records for &'a IterRecords where &'a I: IntoRecords, { type Iter = &'a I; fn iter_rows(self) -> ::IterRows { (&self.iter).iter_rows() } fn count_columns(&self) -> usize { self.count_columns } fn hint_count_rows(&self) -> Option { self.count_rows } } papergrid-0.14.0/src/records/mod.rs000064400000000000000000000021601046102023000152370ustar 00000000000000//! The module contains a [Records] abstraction of a [`Grid`] trait and its implementers. //! //! [`Grid`]: crate::grid::iterable::Grid mod exact_records; mod into_records; mod iter_records; mod peekable_records; pub use exact_records::ExactRecords; pub use into_records::IntoRecords; pub use iter_records::IterRecords; pub use peekable_records::PeekableRecords; #[cfg(feature = "std")] pub mod vec_records; /// Records represents table data. pub trait Records { /// Iterator which goes over rows. type Iter: IntoRecords; /// Returns a iterator over rows. fn iter_rows(self) -> ::IterRows; /// Returns count of columns in the records. fn count_columns(&self) -> usize; /// Hint amount of rows in the records. fn hint_count_rows(&self) -> Option; } // todo: Provide a convenient way to iter over columns // // probably fn iter_columns(self) -> Option // // it'll likely speed up some algos // note: // Maybe simplify IntoRecords; we know count columns any way.... // and sometimes buffering and stuff hard to implement with this laye of abstraction papergrid-0.14.0/src/records/peekable_records.rs000064400000000000000000000027411046102023000177560ustar 00000000000000use crate::config::Position; /// The representation of data, rows and columns of a grid. pub trait PeekableRecords { /// Returns a text of a cell by an index. fn get_text(&self, pos: Position) -> &str; /// Returns a line of a text of a cell by an index. fn get_line(&self, pos: Position, line: usize) -> &str { self.get_text(pos).lines().nth(line).unwrap() } /// Returns an amount of lines of a text of a cell by an index. fn count_lines(&self, pos: Position) -> usize { self.get_text(pos).lines().count() } /// Returns a width of a text of a cell by an index. fn get_width(&self, pos: Position) -> usize { crate::util::string::get_text_width(self.get_text(pos)) } /// Returns a width of line of a text of a cell by an index. fn get_line_width(&self, pos: Position, line: usize) -> usize { crate::util::string::get_line_width(self.get_line(pos, line)) } } impl PeekableRecords for &R where R: PeekableRecords, { fn get_text(&self, pos: Position) -> &str { R::get_text(self, pos) } fn get_line(&self, pos: Position, line: usize) -> &str { R::get_line(self, pos, line) } fn count_lines(&self, pos: Position) -> usize { R::count_lines(self, pos) } fn get_width(&self, pos: Position) -> usize { R::get_width(self, pos) } fn get_line_width(&self, pos: Position, line: usize) -> usize { R::get_line_width(self, pos, line) } } papergrid-0.14.0/src/records/vec_records/cell.rs000064400000000000000000000007631046102023000177040ustar 00000000000000/// Cell implementation which can be used with [`VecRecords`]. /// /// [`VecRecords`]: crate::records::vec_records::VecRecords pub trait Cell { /// Gets a text. fn text(&self) -> &str; /// Gets a line by index. fn line(&self, line: usize) -> &str; /// Returns a number of lines cell has. fn count_lines(&self) -> usize; /// Returns a width of cell. fn width(&self) -> usize; /// Returns a width of cell line. fn line_width(&self, line: usize) -> usize; } papergrid-0.14.0/src/records/vec_records/cell_info.rs000064400000000000000000000111471046102023000207150ustar 00000000000000use core::fmt::Display; use std::{borrow::Cow, cmp::max}; use crate::{ records::vec_records::Cell, util::string::{self, count_lines, get_line_width, get_lines}, }; /// The struct is a [Cell] implementation which keeps width information pre allocated. #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct Text { text: S, width: usize, lines: Vec>, } impl Text { /// Creates a new instance of the structure. pub fn new(text: S) -> Self where S: AsRef, { create_cell_info(text) } /// Creates a new instance of the structure with a single line. pub fn exact(text: S, width: usize, lines: Vec>) -> Self { Self { text, width, lines } } /// Return a original text value. pub fn into_inner(self) -> S { self.text } } impl AsRef for Text where S: AsRef, { fn as_ref(&self) -> &str { self.text() } } impl Display for Text where S: Display, { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.text.fmt(f) } } impl Cell for Text where S: AsRef, { fn text(&self) -> &str { self.text.as_ref() } fn line(&self, i: usize) -> &str { if i == 0 && self.lines.is_empty() { return self.text.as_ref(); } &self.lines[i].text } fn count_lines(&self) -> usize { std::cmp::max(1, self.lines.len()) } fn width(&self) -> usize { self.width } fn line_width(&self, i: usize) -> usize { if i == 0 && self.lines.is_empty() { return self.width; } self.lines[i].width } } impl Clone for Text where S: Clone + AsRef, { fn clone(&self) -> Self { let mut cell = Self { text: self.text.clone(), width: self.width, lines: vec![StrWithWidth::default(); self.lines.len()], }; for (i, line) in self.lines.iter().enumerate() { // We need to redirect pointers to the original string. // // # Safety // // It must be safe because the referenced string and the references are dropped at the same time. // And the referenced String is guaranteed to not be changed. let text = unsafe { let text_ptr = self.text.as_ref().as_ptr(); let line_ptr = line.text.as_ptr(); let text_shift = line_ptr as isize - text_ptr as isize; let new_text_shifted_ptr = cell.text.as_ref().as_ptr().offset(text_shift); std::str::from_utf8_unchecked(std::slice::from_raw_parts( new_text_shifted_ptr, line.text.len(), )) }; cell.lines[i].width = line.width; cell.lines[i].text = Cow::Borrowed(text); } cell } } /// StrWithWidth is a structure is responsible for a string and it's width. #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct StrWithWidth<'a> { text: Cow<'a, str>, width: usize, } impl<'a> StrWithWidth<'a> { /// Creates a new object. pub fn new(text: Cow<'a, str>, width: usize) -> Self { Self { text, width } } } fn create_cell_info>(text: S) -> Text { let mut info = Text { text, lines: vec![], width: 0, }; // Here we do a small optimization. // We check if there's only 1 line in which case we don't allocate lines Vec let count_lines = count_lines(info.text.as_ref()); if count_lines < 2 { info.width = string::get_text_width(info.text.as_ref()); return info; } // In case `Cow::Borrowed` we want to not allocate a String. // It's currerently not possible due to a lifetime issues. (It's known as self-referential struct) // // Here we change the lifetime of text. // // # Safety // // It must be safe because the referenced string and the references are dropped at the same time. // And the referenced String is guaranteed to not be changed. let text = unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts( info.text.as_ref().as_ptr(), info.text.as_ref().len(), )) }; info.lines = vec![StrWithWidth::new(Cow::Borrowed(""), 0); count_lines]; for (line, i) in get_lines(text).zip(info.lines.iter_mut()) { i.width = get_line_width(&line); i.text = line; info.width = max(info.width, i.width); } info } papergrid-0.14.0/src/records/vec_records/mod.rs000064400000000000000000000050671046102023000175460ustar 00000000000000//! Module contains [`VecRecords`]. mod cell; mod cell_info; use crate::{ config::Position, records::{ExactRecords, IntoRecords, Records}, }; use std::ops::{Deref, DerefMut}; use super::PeekableRecords; pub use cell::Cell; pub use cell_info::{StrWithWidth, Text}; /// A [Records] implementation based on allocated buffers. #[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct VecRecords { data: Vec>, shape: (usize, usize), } impl VecRecords { /// Creates new [`VecRecords`] structure. /// /// It assumes that data vector has all rows has the same length(). pub fn new(data: Vec>) -> Self { let count_columns = data.first().map_or(0, |row| row.len()); let count_rows = data.len(); let shape = (count_rows, count_columns); Self { data, shape } } } impl Records for VecRecords { type Iter = Vec>; fn iter_rows(self) -> ::IterRows { self.data.iter_rows() } fn count_columns(&self) -> usize { self.shape.1 } fn hint_count_rows(&self) -> Option { Some(self.shape.0) } } impl<'a, T> Records for &'a VecRecords { type Iter = &'a [Vec]; fn iter_rows(self) -> ::IterRows { (&self.data).iter_rows() } fn count_columns(&self) -> usize { self.shape.1 } fn hint_count_rows(&self) -> Option { Some(self.shape.0) } } impl ExactRecords for VecRecords { fn count_rows(&self) -> usize { self.shape.0 } } impl PeekableRecords for VecRecords where T: Cell, { fn get_text(&self, pos: Position) -> &str { self[pos.row()][pos.col()].text() } fn count_lines(&self, pos: Position) -> usize { self[pos.row()][pos.col()].count_lines() } fn get_line(&self, pos: Position, line: usize) -> &str { self[pos.row()][pos.col()].line(line) } fn get_line_width(&self, pos: Position, line: usize) -> usize { self[pos.row()][pos.col()].line_width(line) } fn get_width(&self, pos: Position) -> usize { self[pos.row()][pos.col()].width() } } impl Deref for VecRecords { type Target = Vec>; fn deref(&self) -> &Self::Target { &self.data } } impl DerefMut for VecRecords { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.data } } impl From> for Vec> { fn from(records: VecRecords) -> Self { records.data } } papergrid-0.14.0/src/util/mod.rs000064400000000000000000000001161046102023000145520ustar 00000000000000//! A module contains utility functions which grid relay on. pub mod string; papergrid-0.14.0/src/util/string.rs000064400000000000000000000157571046102023000153220ustar 00000000000000//! This module contains a different functions which are used by the [`Grid`]. //! //! You should use it if you want to comply with how [`Grid`]. //! //! [`Grid`]: crate::grid::iterable::Grid /// Returns string width and count lines of a string. /// It's a combination of [`get_text_width`] and [`count_lines`]. #[cfg(feature = "std")] pub fn get_text_dimension(text: &str) -> (usize, usize) { get_lines(text) .map(|line| get_line_width(&line)) .fold((0, 0), |(i, acc), width| (i + 1, acc.max(width))) } /// Returns a string width. pub fn get_line_width(text: &str) -> usize { #[cfg(not(feature = "ansi"))] { get_string_width(text) } #[cfg(feature = "ansi")] { // we need to strip ansi because of terminal links // and they're can't be stripped by ansi_str. ansitok::parse_ansi(text) .filter(|e| e.kind() == ansitok::ElementKind::Text) .map(|e| &text[e.start()..e.end()]) .map(get_string_width) .sum() } } /// Returns a max string width of a line. pub fn get_text_width(text: &str) -> usize { text.lines().map(get_line_width).max().unwrap_or(0) } /// Returns a char width. pub fn get_char_width(c: char) -> usize { unicode_width::UnicodeWidthChar::width(c).unwrap_or_default() } /// Returns a string width (accouting all characters). pub fn get_string_width(text: &str) -> usize { unicode_width::UnicodeWidthStr::width(text) } /// Calculates a number of lines. pub fn count_lines(s: &str) -> usize { if s.is_empty() { return 1; } bytecount::count(s.as_bytes(), b'\n') + 1 } /// Returns a list of tabs (`\t`) in a string.. pub fn count_tabs(s: &str) -> usize { bytecount::count(s.as_bytes(), b'\t') } /// Splits the string by lines. #[cfg(feature = "std")] pub fn get_lines(text: &str) -> Lines<'_> { #[cfg(not(feature = "ansi"))] { // we call `split()` but not `lines()` in order to match colored implementation // specifically how we treat a trailing '\n' character. Lines { inner: text.split('\n'), } } #[cfg(feature = "ansi")] { Lines { inner: ansi_str::AnsiStr::ansi_split(text, "\n"), } } } /// Iterator over lines. /// /// In comparison to `std::str::Lines`, it treats trailing '\n' as a new line. #[allow(missing_debug_implementations)] #[cfg(feature = "std")] pub struct Lines<'a> { #[cfg(not(feature = "ansi"))] inner: std::str::Split<'a, char>, #[cfg(feature = "ansi")] inner: ansi_str::AnsiSplit<'a>, } #[cfg(feature = "std")] impl<'a> Iterator for Lines<'a> { type Item = std::borrow::Cow<'a, str>; fn next(&mut self) -> Option { #[cfg(not(feature = "ansi"))] { self.inner.next().map(std::borrow::Cow::Borrowed) } #[cfg(feature = "ansi")] { self.inner.next() } } } #[cfg(feature = "std")] /// Replaces tabs in a string with a given width of spaces. pub fn replace_tab(text: &str, n: usize) -> std::borrow::Cow<'_, str> { if !text.contains('\t') { return std::borrow::Cow::Borrowed(text); } // it's a general case which probably must be faster? let replaced = if n == 4 { text.replace('\t', " ") } else { let mut text = text.to_owned(); replace_tab_range(&mut text, n); text }; std::borrow::Cow::Owned(replaced) } #[cfg(feature = "std")] fn replace_tab_range(cell: &mut String, n: usize) -> &str { let mut skip = 0; while let &Some(pos) = &cell[skip..].find('\t') { let pos = skip + pos; let is_escaped = pos > 0 && cell.get(pos - 1..pos) == Some("\\"); if is_escaped { skip = pos + 1; } else if n == 0 { cell.remove(pos); skip = pos; } else { // I'am not sure which version is faster a loop of 'replace' // or allacation of a string for replacement; cell.replace_range(pos..=pos, &" ".repeat(n)); skip = pos + 1; } if cell.is_empty() || skip >= cell.len() { break; } } cell } #[cfg(test)] mod tests { use super::*; #[test] fn string_width_emojie_test() { // ...emojis such as “joy”, which normally take up two columns when printed in a terminal // https://github.com/mgeisler/textwrap/pull/276 assert_eq!(get_line_width("🎩"), 2); assert_eq!(get_line_width("Rust 💕"), 7); assert_eq!(get_text_width("Go 👍\nC 😎"), 5); } #[cfg(feature = "ansi")] #[test] fn colored_string_width_test() { assert_eq!(get_line_width("\u{1b}[34mhello world\u{1b}[0m"), 11); assert_eq!( get_text_width("\u{1b}[34mhello\u{1b}[0m\n\u{1b}[34mworld\u{1b}[0m",), 5 ); assert_eq!(get_line_width("\u{1b}[34m0\u{1b}[0m"), 1); assert_eq!( get_line_width("\u{1b}[34m\u{1b}[34m\u{1b}[34m0\u{1b}[0m"), 1 ); } #[test] fn count_lines_test() { assert_eq!( count_lines("\u{1b}[37mnow is the time for all good men\n\u{1b}[0m"), 2 ); assert_eq!(count_lines("now is the time for all good men\n"), 2); } #[cfg(feature = "ansi")] #[test] fn string_width_multinline_for_link() { assert_eq!( get_text_width( "\u{1b}]8;;file:///home/nushell/asd.zip\u{1b}\\asd.zip\u{1b}]8;;\u{1b}\\" ), 7 ); } #[cfg(feature = "ansi")] #[test] fn string_width_for_link() { assert_eq!( get_line_width( "\u{1b}]8;;file:///home/nushell/asd.zip\u{1b}\\asd.zip\u{1b}]8;;\u{1b}\\" ), 7 ); } #[cfg(feature = "std")] #[test] fn string_dimension_test() { assert_eq!( get_text_dimension("\u{1b}[37mnow is the time for all good men\n\u{1b}[0m"), { #[cfg(feature = "ansi")] { (2, 32) } #[cfg(not(feature = "ansi"))] { (2, 37) } } ); assert_eq!( get_text_dimension("now is the time for all good men\n"), (2, 32) ); assert_eq!(get_text_dimension("asd"), (1, 3)); assert_eq!(get_text_dimension(""), (1, 0)); } #[cfg(feature = "std")] #[test] fn replace_tab_test() { assert_eq!(replace_tab("123\t\tabc\t", 3), "123 abc "); assert_eq!(replace_tab("\t", 0), ""); assert_eq!(replace_tab("\t", 3), " "); assert_eq!(replace_tab("123\tabc", 3), "123 abc"); assert_eq!(replace_tab("123\tabc\tzxc", 0), "123abczxc"); assert_eq!(replace_tab("\\t", 0), "\\t"); assert_eq!(replace_tab("\\t", 4), "\\t"); assert_eq!(replace_tab("123\\tabc", 0), "123\\tabc"); assert_eq!(replace_tab("123\\tabc", 4), "123\\tabc"); } } papergrid-0.14.0/tests/grid/column_span.rs000064400000000000000000000226311046102023000166420ustar 00000000000000#![cfg(feature = "std")] use papergrid::config::{pos, AlignmentHorizontal, Borders, Entity, Indent, Sides}; use crate::util::grid; use testing_table::test_table; test_table!( row_span, grid(2, 2) .config(|cfg|{ cfg.set_column_span(pos(0, 0), 2); cfg.set_alignment_horizontal(Entity::Cell(0, 0), AlignmentHorizontal::Center); }) .build(), "+---+---+" "| 0-0 |" "+---+---+" "|1-0|1-1|" "+---+---+" ); test_table!( miltiline_span, grid(2, 2) .change_cell(pos(0, 0), "0-0\n0-1") .config(|cfg|{ cfg.set_column_span(pos(0, 0), 2); cfg.set_alignment_horizontal(Entity::Cell(0, 0), AlignmentHorizontal::Center); }) .build(), "+---+---+" "| 0-0 |" "| 0-1 |" "+---+---+" "|1-0|1-1|" "+---+---+" ); test_table!( row_span_multilane, grid(4, 3) .data([ ["first line", "", "e.g."], ["0", "1", "2"], ["0", "1", "2"], ["full last line", "", ""], ]) .config(|cfg|{ cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(3, 0), 3); }) .build(), "+-----+----+----+" "|first line|e.g.|" "+-----+----+----+" "|0 |1 |2 |" "+-----+----+----+" "|0 |1 |2 |" "+-----+----+----+" "|full last line |" "+-----+----+----+" ); test_table!( row_span_with_horizontal_ident, grid(3, 2) .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_padding( Entity::Cell(1, 0), Sides::new( Indent::spaced(4), Indent::spaced(4), Indent::zero(), Indent::zero(), ), ); }) .build(), "+-----------+---+" "|0-0 |" "+-----------+---+" "| 1-0 |1-1|" "+-----------+---+" "|2-0 |2-1|" "+-----------+---+" ); test_table!( _row_span_3x3_with_horizontal_ident, grid(3, 3) .config(|cfg| { cfg.set_column_span(pos(0, 0), 3); cfg.set_column_span(pos(1, 0), 2); cfg.set_column_span(pos(2, 0), 2); }) .build(), "+-+-+---+" "|0-0 |" "+-+-+---+" "|1-0|1-2|" "+-+-+---+" "|2-0|2-2|" "+-+-+---+" ); test_table!( _3x3_with_2_colided_row_span_0, grid(3, 3) .change_cell(pos(0, 0), "0-0xxxxxxx") .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 1), 2); }) .build(), "+-----+----+---+" "|0-0xxxxxxx|0-2|" "+-----+----+---+" "|1-0 |1-1 |" "+-----+----+---+" "|2-0 |2-1 |2-2|" "+-----+----+---+" ); test_table!( _3x3_with_2_colided_row_span_1, grid(3, 3) .change_cell(pos(1, 1), "1-1xxxxxxx") .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 1), 2); }) .build(), "+---+-----+----+" "|0-0 |0-2 |" "+---+-----+----+" "|1-0|1-1xxxxxxx|" "+---+-----+----+" "|2-0|2-1 |2-2 |" "+---+-----+----+" ); test_table!( _3x3_with_2_colided_row_span_2, grid(3, 3) .change_cell(pos(1, 1), "1-1xxxxxxx") .change_cell(pos(2, 0), "2-0xxxxxxxxxxxxx") .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 1), 2); }) .build(), "+----------------+-----+----+" "|0-0 |0-2 |" "+----------------+-----+----+" "|1-0 |1-1xxxxxxx|" "+----------------+-----+----+" "|2-0xxxxxxxxxxxxx|2-1 |2-2 |" "+----------------+-----+----+" ); test_table!( _3x3_with_2_colided_row_span_3, grid(3, 3) .change_cell(pos(2, 1), "2-1xxxxxxxxxxxxx") .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 1), 2); }) .build(), "+---+----------------+---+" "|0-0 |0-2|" "+---+----------------+---+" "|1-0|1-1 |" "+---+----------------+---+" "|2-0|2-1xxxxxxxxxxxxx|2-2|" "+---+----------------+---+" ); test_table!( _3x3_with_2_colided_row_span_4, grid(3, 3) .change_cell(pos(0, 2), "0-2xxxxxxxxxxxxx") .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 1), 2); }) .build(), "+---+---+----------------+" "|0-0 |0-2xxxxxxxxxxxxx|" "+---+---+----------------+" "|1-0|1-1 |" "+---+---+----------------+" "|2-0|2-1|2-2 |" "+---+---+----------------+" ); test_table!( spaned_column_in_first_cell_3x3, grid(3, 3) .change_cell(pos(0, 0), "0-0xxxxxxx") .config(|cfg| cfg.set_column_span(pos(0, 0), 2)) .build(), "+-----+----+---+" "|0-0xxxxxxx|0-2|" "+-----+----+---+" "|1-0 |1-1 |1-2|" "+-----+----+---+" "|2-0 |2-1 |2-2|" "+-----+----+---+" ); test_table!( row_span_with_different_length, grid(3, 2) .data([["first row", ""], ["0", "1"], ["a longer second row", ""]]) .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(2, 0), 2); }) .build(), "+---------+---------+" "|first row |" "+---------+---------+" "|0 |1 |" "+---------+---------+" "|a longer second row|" "+---------+---------+" ); test_table!( row_span_with_odd_length, grid(2, 2) .data([["3 ", ""], ["2", "4"]]) .config(|cfg| cfg.set_column_span(pos(0, 0), 2)) .build(), "+--+-+" "|3 |" "+--+-+" "|2 |4|" "+--+-+" ); test_table!( only_row_spaned, grid(3, 2) .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 0), 2); cfg.set_column_span(pos(2, 0), 2); }) .build(), "+-+-+" "|0-0|" "+-+-+" "|1-0|" "+-+-+" "|2-0|" "+-+-+" ); test_table!( grid_2x2_span_test, grid(2, 2) .data([["123", ""], ["asd", "asd"]]) .config(|cfg| cfg.set_column_span(pos(0, 0), 2)) .build(), "+---+---+" "|123 |" "+---+---+" "|asd|asd|" "+---+---+" ); test_table!( grid_2x2_span_2_test_0, grid(2, 2) .data([["1234", ""], ["asdw", ""]]) .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 0), 2); }) .build(), "+--+-+" "|1234|" "+--+-+" "|asdw|" "+--+-+" ); test_table!( grid_2x2_span_2_test_1, grid(2, 2) .data([["1", ""], ["a", ""]]) .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 0), 2); }) .build(), "+++" "|1|" "+++" "|a|" "+++" ); test_table!( row_span_with_no_split_style, grid(2, 2) .config(|cfg| { cfg.set_borders(Borders::default()); cfg.set_column_span(pos(0, 0), 2); cfg.set_alignment_horizontal(Entity::Cell(0, 0), AlignmentHorizontal::Center); }) .build(), " 0-0 " "1-01-1" ); test_table!( _2x3_zero_span_between_cells_0, grid(2, 3) .config(|cfg| cfg.set_column_span(pos(0, 0), 2)) .build(), "+---+---+---+" "|0-0 |0-2|" "+---+---+---+" "|1-0|1-1|1-2|" "+---+---+---+" ); test_table!( _2x3_zero_span_between_cells_1, grid(2, 3) .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 0), 2); }) .build(), "+-+-+---+" "|0-0|0-2|" "+-+-+---+" "|1-0|1-2|" "+-+-+---+" ); test_table!( _2x3_zero_span_at_the_end_0, grid(2, 3) .config(|cfg| { cfg.set_column_span(pos(0, 0), 3); }) .build(), "+---+---+---+" "|0-0 |" "+---+---+---+" "|1-0|1-1|1-2|" "+---+---+---+" ); test_table!( _2x3_zero_span_at_the_end_1, grid(2, 3) .config(|cfg| { cfg.set_column_span(pos(0, 0), 3); cfg.set_column_span(pos(1, 0), 3); }) .build(), "+-+++" "|0-0|" "+-+++" "|1-0|" "+-+++" ); test_table!( zero_span_grid, grid(2, 2) .data([["123", ""], ["asd", "asd"]]) .config(|cfg| { cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 0), 2); }) .build(), "+-+-+" "|123|" "+-+-+" "|asd|" "+-+-+" ); test_table!( zero_span_grid_1, grid(2, 2) .data([["123", ""], ["asd", "asd"]]) .config(|cfg| { cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 1), 2); }) .build(), "+---++" "+123++" "+---++" ); test_table!( zero_span_grid_2, grid(2, 2) .data([["123", "axc"], ["asd", "asd"]]) .config(|cfg| { cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 1), 2); }) .build(), "+---+---+" "+123+axc+" "+---+---+" ); test_table!( zero_span_is_not_handled, grid(2, 2) .config(|cfg| { cfg.set_column_span(pos(0, 1), 0); }) .build(), "+---+---+" "|0-0|0-1|" "+---+---+" "|1-0|1-1|" "+---+---+" ); papergrid-0.14.0/tests/grid/format_configuration.rs000064400000000000000000001135341046102023000205460ustar 00000000000000#![cfg(feature = "std")] use papergrid::config::{pos, AlignmentHorizontal, AlignmentVertical, Entity, Formatting}; use crate::util::grid; use testing_table::static_table; #[test] fn formatting_test() { let tests = [ ( AlignmentHorizontal::Left, AlignmentVertical::Top, Formatting::new(false, false, true), static_table!( "+-------------+----------+" "|A long string| |" "| | |" "| | |" "| |A |" "| | string|" "| |with |" "| | new |" "| |line |" "| | |" "| | |" "| | |" "+-------------+----------+" "|1-0 |1-1 |" "+-------------+----------+" "|A one more |... |" "| string | |" "|with | |" "| new | |" "|line | |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Left, AlignmentVertical::Top, Formatting::new(true, false, true), static_table!( "+-------------+----------+" "|A long string| |" "| | |" "| | |" "| |A |" "| |string |" "| |with |" "| |new |" "| |line |" "| | |" "| | |" "| | |" "+-------------+----------+" "|1-0 |1-1 |" "+-------------+----------+" "|A one more |... |" "|string | |" "|with | |" "|new | |" "|line | |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Left, AlignmentVertical::Top, Formatting::new(true, true, true), static_table!( "+-------------+----------+" "|A long string|A |" "| |string |" "| |with |" "| |new |" "| |line |" "| | |" "| | |" "| | |" "| | |" "| | |" "| | |" "+-------------+----------+" "|1-0 |1-1 |" "+-------------+----------+" "|A one more |... |" "|string | |" "|with | |" "|new | |" "|line | |" "+-------------+----------+" ), ), // ( AlignmentHorizontal::Center, AlignmentVertical::Top, Formatting::new(false, false, true), static_table!( "+-------------+----------+" "|A long string| |" "| | |" "| | |" "| | A |" "| | string|" "| | with |" "| | new |" "| | line |" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0 | 1-1 |" "+-------------+----------+" "| A one more | ... |" "| string | |" "| with | |" "| new | |" "| line | |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Center, AlignmentVertical::Top, Formatting::new(true, false, true), static_table!( "+-------------+----------+" "|A long string| |" "| | |" "| | |" "| | A |" "| | string |" "| | with |" "| | new |" "| | line |" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0 | 1-1 |" "+-------------+----------+" "| A one more | ... |" "| string | |" "| with | |" "| new | |" "| line | |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Center, AlignmentVertical::Top, Formatting::new(true, true, true), static_table!( "+-------------+----------+" "|A long string| A |" "| | string |" "| | with |" "| | new |" "| | line |" "| | |" "| | |" "| | |" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0 | 1-1 |" "+-------------+----------+" "| A one more | ... |" "| string | |" "| with | |" "| new | |" "| line | |" "+-------------+----------+" ), ), // ( AlignmentHorizontal::Right, AlignmentVertical::Top, Formatting::new(false, false, true), static_table!( "+-------------+----------+" "|A long string| |" "| | |" "| | |" "| | A|" "| | string|" "| | with|" "| | new|" "| | line|" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0| 1-1|" "+-------------+----------+" "| A one more| ...|" "| string| |" "| with| |" "| new| |" "| line| |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Right, AlignmentVertical::Top, Formatting::new(true, false, true), static_table!( "+-------------+----------+" "|A long string| |" "| | |" "| | |" "| | A|" "| | string|" "| | with|" "| | new|" "| | line|" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0| 1-1|" "+-------------+----------+" "| A one more| ...|" "| string| |" "| with| |" "| new| |" "| line| |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Right, AlignmentVertical::Top, Formatting::new(true, true, true), static_table!( "+-------------+----------+" "|A long string| A|" "| | string|" "| | with|" "| | new|" "| | line|" "| | |" "| | |" "| | |" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0| 1-1|" "+-------------+----------+" "| A one more| ...|" "| string| |" "| with| |" "| new| |" "| line| |" "+-------------+----------+" ), ), // asd ( AlignmentHorizontal::Left, AlignmentVertical::Center, Formatting::new(false, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| |A |" "| | string|" "|A long string|with |" "| | new |" "| |line |" "| | |" "| | |" "| | |" "+-------------+----------+" "|1-0 |1-1 |" "+-------------+----------+" "|A one more | |" "| string | |" "|with |... |" "| new | |" "|line | |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Left, AlignmentVertical::Center, Formatting::new(true, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| |A |" "| |string |" "|A long string|with |" "| |new |" "| |line |" "| | |" "| | |" "| | |" "+-------------+----------+" "|1-0 |1-1 |" "+-------------+----------+" "|A one more | |" "|string | |" "|with |... |" "|new | |" "|line | |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Left, AlignmentVertical::Center, Formatting::new(true, true, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| |A |" "| |string |" "|A long string|with |" "| |new |" "| |line |" "| | |" "| | |" "| | |" "+-------------+----------+" "|1-0 |1-1 |" "+-------------+----------+" "|A one more | |" "|string | |" "|with |... |" "|new | |" "|line | |" "+-------------+----------+" ), ), // ( AlignmentHorizontal::Center, AlignmentVertical::Center, Formatting::new(false, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | A |" "| | string|" "|A long string| with |" "| | new |" "| | line |" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0 | 1-1 |" "+-------------+----------+" "| A one more | |" "| string | |" "| with | ... |" "| new | |" "| line | |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Center, AlignmentVertical::Center, Formatting::new(true, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | A |" "| | string |" "|A long string| with |" "| | new |" "| | line |" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0 | 1-1 |" "+-------------+----------+" "| A one more | |" "| string | |" "| with | ... |" "| new | |" "| line | |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Center, AlignmentVertical::Center, Formatting::new(true, true, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | A |" "| | string |" "|A long string| with |" "| | new |" "| | line |" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0 | 1-1 |" "+-------------+----------+" "| A one more | |" "| string | |" "| with | ... |" "| new | |" "| line | |" "+-------------+----------+" ), ), // ( AlignmentHorizontal::Right, AlignmentVertical::Center, Formatting::new(false, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | A|" "| | string|" "|A long string| with|" "| | new|" "| | line|" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0| 1-1|" "+-------------+----------+" "| A one more| |" "| string| |" "| with| ...|" "| new| |" "| line| |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Right, AlignmentVertical::Center, Formatting::new(true, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | A|" "| | string|" "|A long string| with|" "| | new|" "| | line|" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0| 1-1|" "+-------------+----------+" "| A one more| |" "| string| |" "| with| ...|" "| new| |" "| line| |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Right, AlignmentVertical::Center, Formatting::new(true, true, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | A|" "| | string|" "|A long string| with|" "| | new|" "| | line|" "| | |" "| | |" "| | |" "+-------------+----------+" "| 1-0| 1-1|" "+-------------+----------+" "| A one more| |" "| string| |" "| with| ...|" "| new| |" "| line| |" "+-------------+----------+" ), ), // // asd ( AlignmentHorizontal::Left, AlignmentVertical::Bottom, Formatting::new(false, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| |A |" "| | string|" "| |with |" "| | new |" "| |line |" "| | |" "| | |" "|A long string| |" "+-------------+----------+" "|1-0 |1-1 |" "+-------------+----------+" "|A one more | |" "| string | |" "|with | |" "| new | |" "|line |... |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Left, AlignmentVertical::Bottom, Formatting::new(true, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| |A |" "| |string |" "| |with |" "| |new |" "| |line |" "| | |" "| | |" "|A long string| |" "+-------------+----------+" "|1-0 |1-1 |" "+-------------+----------+" "|A one more | |" "|string | |" "|with | |" "|new | |" "|line |... |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Left, AlignmentVertical::Bottom, Formatting::new(true, true, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | |" "| | |" "| | |" "| |A |" "| |string |" "| |with |" "| |new |" "|A long string|line |" "+-------------+----------+" "|1-0 |1-1 |" "+-------------+----------+" "|A one more | |" "|string | |" "|with | |" "|new | |" "|line |... |" "+-------------+----------+" ), ), // ( AlignmentHorizontal::Center, AlignmentVertical::Bottom, Formatting::new(false, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | A |" "| | string|" "| | with |" "| | new |" "| | line |" "| | |" "| | |" "|A long string| |" "+-------------+----------+" "| 1-0 | 1-1 |" "+-------------+----------+" "| A one more | |" "| string | |" "| with | |" "| new | |" "| line | ... |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Center, AlignmentVertical::Bottom, Formatting::new(true, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | A |" "| | string |" "| | with |" "| | new |" "| | line |" "| | |" "| | |" "|A long string| |" "+-------------+----------+" "| 1-0 | 1-1 |" "+-------------+----------+" "| A one more | |" "| string | |" "| with | |" "| new | |" "| line | ... |" "+-------------+----------+" ), ), ( AlignmentHorizontal::Center, AlignmentVertical::Bottom, Formatting::new(true, true, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | |" "| | |" "| | |" "| | A |" "| | string |" "| | with |" "| | new |" "|A long string| line |" "+-------------+----------+" "| 1-0 | 1-1 |" "+-------------+----------+" "| A one more | |" "| string | |" "| with | |" "| new | |" "| line | ... |" "+-------------+----------+" ), ), // ( AlignmentHorizontal::Right, AlignmentVertical::Bottom, Formatting::new(false, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | A|" "| | string|" "| | with|" "| | new|" "| | line|" "| | |" "| | |" "|A long string| |" "+-------------+----------+" "| 1-0| 1-1|" "+-------------+----------+" "| A one more| |" "| string| |" "| with| |" "| new| |" "| line| ...|" "+-------------+----------+" ), ), ( AlignmentHorizontal::Right, AlignmentVertical::Bottom, Formatting::new(true, false, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | A|" "| | string|" "| | with|" "| | new|" "| | line|" "| | |" "| | |" "|A long string| |" "+-------------+----------+" "| 1-0| 1-1|" "+-------------+----------+" "| A one more| |" "| string| |" "| with| |" "| new| |" "| line| ...|" "+-------------+----------+" ), ), ( AlignmentHorizontal::Right, AlignmentVertical::Bottom, Formatting::new(true, true, true), static_table!( "+-------------+----------+" "| | |" "| | |" "| | |" "| | |" "| | |" "| | |" "| | A|" "| | string|" "| | with|" "| | new|" "|A long string| line|" "+-------------+----------+" "| 1-0| 1-1|" "+-------------+----------+" "| A one more| |" "| string| |" "| with| |" "| new| |" "| line| ...|" "+-------------+----------+" ), ), ]; let grid = grid(3, 2) .change_cell(pos(0, 0), "A long string") .change_cell(pos(0, 1), "\n\n\nA\n string\nwith\n new\nline\n\n\n") .change_cell(pos(2, 0), "A one more\n string\nwith\n new\nline") .change_cell(pos(2, 1), "..."); for (i, test) in tests.iter().enumerate() { let table = grid .clone() .config(|cfg| { cfg.set_alignment_horizontal(Entity::Global, test.0); cfg.set_alignment_vertical(Entity::Global, test.1); cfg.set_line_alignment(Entity::Global, test.2.allow_lines_alignment); cfg.set_trim_horizontal(Entity::Global, test.2.horizontal_trim); cfg.set_trim_vertical(Entity::Global, test.2.vertical_trim); }) .clone() .build(); let expected = test.3; assert_eq!(table, expected, "test case #{i:?} failed"); } } #[test] fn formatting_empty_test() { for (rows, cols) in [(0, 0), (0, 4), (4, 0)] { let formatting = Formatting::new(true, true, true); assert_eq!( grid(rows, cols) .config(|cfg| { cfg.set_line_alignment(Entity::Global, formatting.allow_lines_alignment); cfg.set_trim_horizontal(Entity::Global, formatting.horizontal_trim); cfg.set_trim_vertical(Entity::Global, formatting.vertical_trim); }) .build(), "" ); } } #[test] fn formatting_1x1_test() { let json = r#" { "id": "0001", "batters": { "batter": [ { "id": "1002", "type": "Chocolate" }, ] }, "topping": [ { "id": "5003", "type": "Chocolate" }, { "id": "5004", "type": "Maple" } ] }"#; let grid = grid(1, 1).data([[json]]); assert_eq!( grid.clone() .config( |cfg| cfg.set_alignment_horizontal(Entity::Cell(0, 0), AlignmentHorizontal::Left) ) .build(), static_table!( r#"+--------------------------------------------------+"# r#"| |"# r#"|{ |"# r#"| "id": "0001", |"# r#"| "batters": { |"# r#"| "batter": [ |"# r#"| { "id": "1002", "type": "Chocolate" },|"# r#"| ] |"# r#"| }, |"# r#"| "topping": [ |"# r#"| { "id": "5003", "type": "Chocolate" }, |"# r#"| { "id": "5004", "type": "Maple" } |"# r#"| ] |"# r#"|} |"# r#"+--------------------------------------------------+"# ), ); assert_eq!( grid.clone() .config(|cfg| cfg.set_line_alignment(Entity::Global, true)) .build(), static_table!( r#"+--------------------------------------------------+"# r#"| |"# r#"|{ |"# r#"| "id": "0001", |"# r#"| "batters": { |"# r#"| "batter": [ |"# r#"| { "id": "1002", "type": "Chocolate" },|"# r#"| ] |"# r#"| }, |"# r#"| "topping": [ |"# r#"| { "id": "5003", "type": "Chocolate" }, |"# r#"| { "id": "5004", "type": "Maple" } |"# r#"| ] |"# r#"|} |"# r#"+--------------------------------------------------+"# ), ); assert_eq!( grid.clone() .config(|cfg| cfg.set_line_alignment(Entity::Global, true)) .config(|cfg| cfg.set_trim_horizontal(Entity::Global, true)) .build(), static_table!( r#"+--------------------------------------------------+"# r#"| |"# r#"|{ |"# r#"|"id": "0001", |"# r#"|"batters": { |"# r#"|"batter": [ |"# r#"|{ "id": "1002", "type": "Chocolate" }, |"# r#"|] |"# r#"|}, |"# r#"|"topping": [ |"# r#"|{ "id": "5003", "type": "Chocolate" }, |"# r#"|{ "id": "5004", "type": "Maple" } |"# r#"|] |"# r#"|} |"# r#"+--------------------------------------------------+"# ), ); assert_eq!( grid.config(|cfg| cfg.set_line_alignment(Entity::Global, true)) .config(|cfg| cfg.set_trim_horizontal(Entity::Global, true)) .config(|cfg| cfg.set_trim_vertical(Entity::Global, true)) .build(), static_table!( r#"+--------------------------------------------------+"# r#"|{ |"# r#"|"id": "0001", |"# r#"|"batters": { |"# r#"|"batter": [ |"# r#"|{ "id": "1002", "type": "Chocolate" }, |"# r#"|] |"# r#"|}, |"# r#"|"topping": [ |"# r#"|{ "id": "5003", "type": "Chocolate" }, |"# r#"|{ "id": "5004", "type": "Maple" } |"# r#"|] |"# r#"|} |"# r#"| |"# r#"+--------------------------------------------------+"# ), ); } #[test] fn tabs_arent_handled() { let json = "{ \t\t \"id\": \"1\", \t\t \"name\": \"Hello World\", \t\t \"list\": [ \t\t\t\t [1, 2, 3], \t\t\t\t [4, 5, 6], \t\t ] }"; let grid = grid(1, 1).data([[json]]); println!("{}", grid.clone().build()); assert_eq!( grid.build(), static_table!( "+-------------------------+" "|{ |" "|\t\t \"id\": \"1\", |" "|\t\t \"name\": \"Hello World\",|" "|\t\t \"list\": [ |" "|\t\t\t\t [1, 2, 3], |" "|\t\t\t\t [4, 5, 6], |" "|\t\t ] |" "|} |" "+-------------------------+" ), ); } papergrid-0.14.0/tests/grid/mod.rs000064400000000000000000000001631046102023000150770ustar 00000000000000mod column_span; mod format_configuration; mod peekable_grid; mod render; mod row_span; mod settings; mod styling; papergrid-0.14.0/tests/grid/peekable_grid.rs000064400000000000000000000130411046102023000170740ustar 00000000000000#![cfg(feature = "std")] use papergrid::{ colors::NoColors, config::{ spanned::SpannedConfig, AlignmentHorizontal, AlignmentVertical, Borders, Entity, Indent, Sides, }, dimension::{spanned::SpannedGridDimension, Dimension}, grid::peekable::PeekableGrid, records::vec_records::{Text, VecRecords}, }; use testing_table::test_table; struct Dims { width: Vec, height: Vec, } impl Dimension for Dims { fn get_width(&self, column: usize) -> usize { self.width[column] } fn get_height(&self, row: usize) -> usize { self.height[row] } } test_table!( continues_empty_rows_with_horizontal_lines, { let mut cfg = SpannedConfig::default(); cfg.set_borders(Borders { top: Some('-'), top_left: Some('+'), top_right: Some('+'), top_intersection: Some('+'), bottom: Some('-'), bottom_left: Some('+'), bottom_right: Some('+'), bottom_intersection: Some('+'), horizontal: Some('-'), left_intersection: Some('+'), right_intersection: Some('+'), vertical: Some('|'), left: Some('|'), right: Some('|'), intersection: Some('+'), }); cfg.set_alignment_horizontal((1, 0).into(), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Entity::Global, AlignmentVertical::Center); cfg.set_padding( (0, 0).into(), Sides::new( Indent::spaced(4), Indent::spaced(4), Indent::spaced(1), Indent::spaced(1), ), ); let data = [ ["Papergrid", "is a library", "for print tables", "!"], ["", "", "", ""], ["", "", "", ""], ["", "", "", ""], ["?", "?", "?", "?"], ["", "", "", ""], ["is a library", "is a library", "is a library", "is a library"], ]; let data = data .iter() .map(|row| row.iter().map(Text::new).collect()) .collect(); let records = VecRecords::new(data); let dims = Dims { width: SpannedGridDimension::width(&records, &cfg), height: vec![3, 0, 0, 0, 1, 0, 1], }; PeekableGrid::new(&records, &cfg, &dims, NoColors).to_string() }, "+-----------------+------------+----------------+------------+" "| | | | |" "| Papergrid |is a library|for print tables|! |" "| | | | |" "+-----------------+------------+----------------+------------+" "+-----------------+------------+----------------+------------+" "+-----------------+------------+----------------+------------+" "+-----------------+------------+----------------+------------+" "|? |? |? |? |" "+-----------------+------------+----------------+------------+" "+-----------------+------------+----------------+------------+" "|is a library |is a library|is a library |is a library|" "+-----------------+------------+----------------+------------+" ); test_table!( continues_empty_rows_with_no_horizontal_lines, { let mut cfg = SpannedConfig::default(); cfg.set_borders(Borders { top: Some('-'), top_left: Some('+'), top_right: Some('+'), top_intersection: Some('+'), bottom: Some('-'), bottom_left: Some('+'), bottom_right: Some('+'), bottom_intersection: Some('+'), horizontal: None, left_intersection: None, right_intersection: None, vertical: Some('|'), left: Some('|'), right: Some('|'), intersection: None, }); cfg.set_alignment_horizontal((1, 0).into(), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Entity::Global, AlignmentVertical::Center); cfg.set_padding( (0, 0).into(), Sides::new( Indent::spaced(4), Indent::spaced(4), Indent::spaced(1), Indent::spaced(1), ), ); let data = [ ["Papergrid", "is a library", "for print tables", "!"], ["", "", "", ""], ["", "", "", ""], ["", "", "", ""], ["?", "?", "?", "?"], ["", "", "", ""], ["is a library", "is a library", "is a library", "is a library"], ]; let data = data .iter() .map(|row| row.iter().map(Text::new).collect()) .collect(); let records = VecRecords::new(data); let dims = Dims { width: SpannedGridDimension::width(&records, &cfg), height: vec![3, 0, 0, 0, 1, 0, 1], }; PeekableGrid::new(&records, &cfg, &dims, NoColors).to_string() }, "+-----------------+------------+----------------+------------+" "| | | | |" "| Papergrid |is a library|for print tables|! |" "| | | | |" "|? |? |? |? |" "|is a library |is a library|is a library |is a library|" "+-----------------+------------+----------------+------------+" ); papergrid-0.14.0/tests/grid/render.rs000064400000000000000000000175571046102023000156160ustar 00000000000000// Copyright (c) 2021 Maxim Zhiburt // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. #![cfg(feature = "std")] use std::vec; use papergrid::{ colors::NoColors, config::{ pos, spanned::SpannedConfig, AlignmentHorizontal, AlignmentVertical, Borders, Entity, }, grid::iterable::Grid, records::IterRecords, }; use crate::util::{grid, ConstantDimension, DEFAULT_BORDERS}; use testing_table::test_table; test_table!(render_0x0, grid(0, 0).build(), ""); test_table!( render_1x1, grid(1, 1).change_cell(pos(0, 0), "one line").build(), "+--------+" "|one line|" "+--------+" ); test_table!( render_1x1_empty, grid(1, 1).change_cell(pos(0, 0), "").build(), "++" "||" "++" ); test_table!( render_1x1_empty_with_height_0, { let data = vec![vec![""]]; let data = IterRecords::new(data, 1, Some(1)); let dims = ConstantDimension(vec![0], vec![0]); let mut cfg = SpannedConfig::default(); cfg.set_borders(DEFAULT_BORDERS); let grid = Grid::new(&data, &dims, &cfg, NoColors); grid.to_string() }, "++" "++" ); test_table!( render_1x1_empty_with_height_with_width, { let data = vec![vec![String::from("")]]; let data = IterRecords::new(&data, 1, Some(1)); let dims = ConstantDimension(vec![10], vec![0]); let mut cfg = SpannedConfig::default(); cfg.set_borders(Borders { top_left: Some('┌'), top_right: Some('┐'), bottom_left: Some('└'), bottom_right: Some('┘'), top: Some('─'), bottom: Some('─'), ..Default::default() }); let grid = Grid::new(data, &dims, &cfg, NoColors); grid.to_string() }, "┌──────────┐" "└──────────┘" ); test_table!( render_2x2, grid(2, 2).build(), "+---+---+" "|0-0|0-1|" "+---+---+" "|1-0|1-1|" "+---+---+" ); test_table!( render_3x2, grid(3, 2).build(), "+---+---+" "|0-0|0-1|" "+---+---+" "|1-0|1-1|" "+---+---+" "|2-0|2-1|" "+---+---+" ); test_table!( render_1x2, grid(1, 2).data([["hello", "world"]]).build(), "+-----+-----+" "|hello|world|" "+-----+-----+" ); test_table!( render_multilane, grid(2, 2) .data([ ["left\ncell", "right one"], ["the second column got the beginning here", "and here\nwe\nsee\na\nlong\nstring"], ]) .build(), "+----------------------------------------+---------+" "|left |right one|" "|cell | |" "+----------------------------------------+---------+" "|the second column got the beginning here|and here |" "| |we |" "| |see |" "| |a |" "| |long |" "| |string |" "+----------------------------------------+---------+" ); test_table!( render_multilane_alignment, grid(2, 2) .config(|cfg|{ cfg.set_alignment_horizontal(Entity::Cell(0, 0), AlignmentHorizontal::Center); cfg.set_alignment_horizontal(Entity::Cell(1, 1), AlignmentHorizontal::Right); }) .data([ ["left\ncell", "right one"], ["the second column got the beginning here", "and here\nwe\nsee\na\nlong\nstring"], ]) .build(), "+----------------------------------------+---------+" "| left |right one|" "| cell | |" "+----------------------------------------+---------+" "|the second column got the beginning here| and here|" "| | we |" "| | see |" "| | a |" "| | long |" "| | string |" "+----------------------------------------+---------+" ); test_table!( render_multilane_vertical_alignment, grid(2, 2) .data([ ["left\ncell", "right one"], ["the second column got the beginning here", "and here\nwe\nsee\na\nlong\nstring"], ]) .config(|cfg|{ cfg.set_alignment_horizontal(Entity::Cell(0, 0), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Entity::Cell(1, 0), AlignmentVertical::Center); cfg.set_alignment_horizontal(Entity::Cell(1, 1), AlignmentHorizontal::Right); }) .build(), "+----------------------------------------+---------+" "| left |right one|" "| cell | |" "+----------------------------------------+---------+" "| | and here|" "| | we |" "|the second column got the beginning here| see |" "| | a |" "| | long |" "| | string |" "+----------------------------------------+---------+" ); test_table!( render_empty_cell, grid(2, 2).change_cell(pos(0, 1), "").build(), "+---+---+" "|0-0| |" "+---+---+" "|1-0|1-1|" "+---+---+" ); test_table!( hieroglyph_multiline_handling, grid(1, 2).data([["哈哈", "哈\n哈"]]).build(), "+----+--+" "|哈哈|哈|" "| |哈|" "+----+--+" ); #[test] fn emoji_width_test() { use papergrid::util::string::get_string_width; assert_eq!(get_string_width("👩"), 2); assert_eq!(get_string_width("🔬"), 2); assert_eq!(get_string_width("👩\u{200D}🔬"), 2); } test_table!( emoji_handling, grid(2, 1).data([["👩👩👩👩👩👩"], ["Hello"]]).build(), "+------------+" "|👩👩👩👩👩👩|" "+------------+" "|Hello |" "+------------+" ); test_table!( emoji_handling_2, grid(2, 1).data([["👩\u{200D}🔬👩\u{200D}🔬👩\u{200D}🔬👩\u{200D}🔬👩\u{200D}🔬👩\u{200D}🔬"], ["Hello"]]).build(), "+------------+" "|👩\u{200D}🔬👩\u{200D}🔬👩\u{200D}🔬👩\u{200D}🔬👩\u{200D}🔬👩\u{200D}🔬|" "+------------+" "|Hello |" "+------------+" ); test_table!( doesnt_render_return_carige_0, grid(2, 2).change_cell(pos(0, 1), "123\r\r\r567").build(), "+---+---------+" "|0-0|123\r\r\r567|" "+---+---------+" "|1-0|1-1 |" "+---+---------+" ); test_table!( doesnt_render_return_carige_1, grid(2, 2).change_cell(pos(1, 1), "12345678").change_cell(pos(0, 1), "123\r\r\r567").build(), "+---+---------+" "|0-0|123\r\r\r567|" "+---+---------+" "|1-0|12345678 |" "+---+---------+" ); // #[test] // #[ignore = "I am not sure what is the right behaviour here"] // fn hieroglyph_handling() { // let grid = util::grid_from([["哈哈", "哈"]]); // assert_eq!( // grid, // "+----+--+\n\ // |哈哈 |哈 |\n\ // +----+--+", // ) // } papergrid-0.14.0/tests/grid/row_span.rs000064400000000000000000000634421046102023000161610ustar 00000000000000#![cfg(feature = "std")] use papergrid::config::{ pos, AlignmentHorizontal, AlignmentVertical, Borders, Entity::{self, *}, Indent, Sides, }; use crate::util::grid; use testing_table::test_table; test_table!( _2x2_vertical_alignment_center, grid(2, 2) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_alignment_vertical(Cell(0, 0), AlignmentVertical::Center); }) .build(), "+---+---+" "| |0-1|" "+0-0+---+" "| |1-1|" "+---+---+" ); test_table!( _2x2_vertical_alignment_bottom, grid(2, 2) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_alignment_vertical(Cell(0, 0), AlignmentVertical::Bottom); }) .build(), "+---+---+" "| |0-1|" "+ +---+" "|0-0|1-1|" "+---+---+" ); test_table!( _2x2_multiline, grid(2, 2) .change_cell(pos(0, 0), "0-0\n0-1xxx") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_alignment_vertical(Cell(0, 0), AlignmentVertical::Center); }) .build(), "+------+---+" "|0-0 |0-1|" "+0-1xxx+---+" "| |1-1|" "+------+---+" ); test_table!( _2x2_multiline_vertical_alignment_bottom, grid(2, 2) .change_cell(pos(0, 0), "0-0\n0-1xxx") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_alignment_vertical(Cell(0, 0), AlignmentVertical::Bottom); }) .build(), "+------+---+" "| |0-1|" "+0-0 +---+" "|0-1xxx|1-1|" "+------+---+" ); test_table!( _4x3_multiline_0, grid(4, 3) .data([ ["first line", "0-1", "full last line"], ["", "1", ""], ["0", "1", ""], ["3-0", "3-1", ""], ]) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 2), 4); }) .build(), "+----------+---+--------------+" "|first line|0-1|full last line|" "+ +---+ +" "| |1 | |" "+----------+---+ +" "|0 |1 | |" "+----------+---+ +" "|3-0 |3-1| |" "+----------+---+--------------+" ); test_table!( _3x2_with_horizontal_ident_on_spanned_cell, grid(3, 2) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_padding(Cell(1, 0), Sides::new(Indent::spaced(4), Indent::spaced(4), Indent::default(), Indent::default())); }) .build(), "+---+---+" "|0-0|0-1|" "+ +---+" "| |1-1|" "+---+---+" "|2-0|2-1|" "+---+---+" ); test_table!( _3x2_with_horizontal_ident, grid(3, 2) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_padding(Cell(0, 0), Sides::new(Indent::spaced(4), Indent::spaced(4), Indent::default(), Indent::default())); }) .build(), "+-----------+---+" "| 0-0 |0-1|" "+ +---+" "| |1-1|" "+-----------+---+" "|2-0 |2-1|" "+-----------+---+" ); test_table!( _3x2_with_vertical_ident, grid(3, 2) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_padding(Cell(0, 0), Sides::new(Indent::default(), Indent::default(), Indent::spaced(4), Indent::spaced(4))); }) .build(), "+---+---+" "| |0-1|" "| | |" "| | |" "| | |" "+0-0+---+" "| |1-1|" "| | |" "| | |" "| | |" "+---+---+" "|2-0|2-1|" "+---+---+" ); test_table!( _3x3_render_0, grid(3, 3) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 3); cfg.set_row_span(pos(0, 1), 2); cfg.set_row_span(pos(0, 2), 2); }) .build(), "+---+---+---+" "+0-0+0-1+0-2+" "+ +---+---+" "| |2-1|2-2|" "+---+---+---+" ); test_table!( _3x3_render_1, grid(3, 3) .change_cell(pos(0, 1), "t\ne\nx\nt") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 3); cfg.set_row_span(pos(0, 1), 2); cfg.set_row_span(pos(0, 2), 2); }) .build(), "+---+---+---+" "|0-0|t |0-2|" "| |e | |" "+ +x + +" "| |t | |" "+ +---+---+" "| |2-1|2-2|" "+---+---+---+" ); test_table!( _3x3_coliison_0, grid(3, 3) .change_cell(pos(0, 0), "0-0xxxxxxx") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(1, 1), 2); }) .build(), "+----------+---+---+" "|0-0xxxxxxx|0-1|0-2|" "+ +---+---+" "| |1-1|1-2|" "+----------+ +---+" "|2-0 | |2-2|" "+----------+---+---+" ); test_table!( _3x3_coliison_1, grid(3, 3) .change_cell(pos(1, 1), "1-1xxxxxxx") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(1, 1), 2); }) .build(), "+---+----------+---+" "|0-0|0-1 |0-2|" "+ +----------+---+" "| |1-1xxxxxxx|1-2|" "+---+ +---+" "|2-0| |2-2|" "+---+----------+---+" ); test_table!( _3x3_coliison_2, grid(3, 3) .change_cell(pos(1, 1), "1-1\nx\nx\nxxxxx") .change_cell(pos(0, 2), "2-0x\nxxx\nxx\nxxxxxxx") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(1, 1), 2); }) .build(), "+---+-----+-------+" "|0-0|0-1 |2-0x |" "| | |xxx |" "| | |xx |" "| | |xxxxxxx|" "+ +-----+-------+" "| |1-1 |1-2 |" "| |x | |" "+---+x +-------+" "|2-0|xxxxx|2-2 |" "+---+-----+-------+" ); test_table!( _3x3_coliison_3, grid(3, 3) .change_cell(pos(1, 2), "2-1\nxx\nxx\nxx\nxxxxxx\nx") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(1, 1), 2); }) .build(), "+---+---+------+" "|0-0|0-1|0-2 |" "+ +---+------+" "| |1-1|2-1 |" "| | |xx |" "| | |xx |" "| | |xx |" "| | |xxxxxx|" "| | |x |" "+---+ +------+" "|2-0| |2-2 |" "+---+---+------+" ); test_table!( _3x3_coliison_4, grid(3, 3) .change_cell(pos(2, 1), "0-2\nx\nx\nx\nx\nxxxxxxx\nx\nx") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(1, 2), 2); }) .build(), "+---+-------+---+" "|0-0|0-1 |0-2|" "+ +-------+---+" "| |1-1 |1-2|" "+---+-------+ +" "|2-0|0-2 | |" "| |x | |" "| |x | |" "| |x | |" "| |x | |" "| |xxxxxxx| |" "| |x | |" "| |x | |" "+---+-------+---+" ); test_table!( _3x3_first_row, grid(3, 3) .change_cell(pos(0, 0), "0-0\nxx\nx\nx\nx\nx\nx") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); }) .build(), "+---+---+---+" "|0-0|0-1|0-2|" "|xx | | |" "|x | | |" "+x +---+---+" "|x |1-1|1-2|" "|x | | |" "|x | | |" "+---+---+---+" "|2-0|2-1|2-2|" "+---+---+---+" ); test_table!( _2x3_with_different_length, grid(2, 3) .change_cell(pos(0, 0), "f\nir\nst\n ro\nw") .change_cell(pos(0, 2), "a\n \nlonger\n \nsecond\n \nrow") .change_cell(pos(1, 0), "0") .change_cell(pos(1, 1), "1") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 2), 2); }) .build(), "+---+---+------+" "|f |0-1|a |" "|ir | | |" "|st | |longer|" "+ ro+---+ +" "|w |1 |second|" "| | | |" "| | |row |" "+---+---+------+" ); test_table!( _2x2_with_odd_length, grid(2, 2) .change_cell(pos(0, 0), "3\n \n \n ") .change_cell(pos(0, 1), "2") .change_cell(pos(1, 1), "4") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); }) .build(), "+-+-+" "|3|2|" "| | |" "+ +-+" "| |4|" "+-+-+" ); test_table!( _2x3_only_col_spaned, grid(2, 3) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 1), 2); cfg.set_row_span(pos(0, 2), 2); }) .build(), "+---+---+---+" "+0-0+0-1+0-2+" "+---+---+---+" ); test_table!( _2x2_render_0, grid(2, 2) .change_cell(pos(0, 0), "1\n\n\n\n\n\n\n23") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); }) .build(), "+--+---+" "|1 |0-1|" "| | |" "| | |" "| | |" "+ +---+" "| |1-1|" "| | |" "|23| |" "+--+---+" ); test_table!( _2x2_render_1, grid(2, 2) .data([["12\n3\n4", "a\ns\ndw"], ["asd", "asd"]]) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 1), 2); }) .build(), "+--+--+" "|12|a |" "+3 +s +" "|4 |dw|" "+--+--+" ); test_table!( _2x2_render_2, grid(2, 2) .data([["1", "a"], ["asd", "asd"]]) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 1), 2); }) .build(), "+-+-+" "+1+a+" "+-+-+" ); test_table!( _2x2_render_3, grid(2, 2) .data([["1as\nd\n", "a"], ["as\ndasdds\na", "asd"]]) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 1), 2); }) .build(), "+---+-+" "|1as|a|" "+d + +" "| | |" "+---+-+" ); test_table!( _2x2_render_4, grid(2, 2) .data([["1as\nd\n", "a"], ["as\ndasdds\na", "asd"]]) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 1), 2); cfg.set_alignment_vertical(Entity::Global, AlignmentVertical::Center) }) .build(), "+---+-+" "|1as| |" "+d +a+" "| | |" "+---+-+" ); test_table!( _2x2_render_5, grid(2, 2) .data([["1a\ns\nd\n", "a"], ["as\ndasdds\na", "asd"]]) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 1), 2); cfg.set_alignment_vertical(Entity::Global, AlignmentVertical::Center) }) .build(), "+--+-+" "|1a| |" "|s |a|" "+d + +" "| | |" "+--+-+" ); test_table!( _2x2_render_6, grid(2, 2) .data([["1a\ns\nd", "a"], ["as\ndasdds\na", "asd"]]) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 1), 2); cfg.set_alignment_vertical(Entity::Global, AlignmentVertical::Bottom) }) .build(), "+--+-+" "|1a| |" "+s + +" "|d |a|" "+--+-+" ); test_table!( _2x2_with_no_split_style, grid(2, 2) .change_cell(pos(0, 0), "1\n2\n3") .config(|cfg|{ cfg.set_borders(Borders::default()); cfg.set_row_span(pos(0, 0), 2); cfg.set_alignment_vertical(Cell(0, 0), AlignmentVertical::Center); }) .build(), "10-1" "2 " "31-1" ); test_table!( _3x2_with_zero_row_span_0, grid(3, 2) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); }) .build(), "+---+---+" "|0-0|0-1|" "+ +---+" "| |1-1|" "+---+---+" "|2-0|2-1|" "+---+---+" ); test_table!( _3x2_with_zero_row_span_1, grid(3, 2) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 1), 2); }) .build(), "+---+---+" "+0-0+0-1+" "+---+---+" "|2-0|2-1|" "+---+---+" ); test_table!( _3x2_with_zero_row_span_2, grid(3, 2) .config(|cfg|{ cfg.set_row_span(pos(0, 1), 3); }) .build(), "+---+---+" "|0-0|0-1|" "+---+ +" "|1-0| |" "+---+ +" "|2-0| |" "+---+---+" ); test_table!( _3x2_with_zero_row_span_3, grid(3, 2) .config(|cfg|{ cfg.set_row_span(pos(0, 1), 3); cfg.set_row_span(pos(0, 0), 3); }) .build(), "+---+---+" "+0-0+0-1+" "+ + +" "+---+---+" ); test_table!( _2x2_with_zero_row_span_4, grid(2, 2) .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 1), 2); }) .build(), "+---+---+" "+0-0+0-1+" "+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_0, grid(4, 4) .change_cell(pos(1, 1), "123\n345\n555\n333") .config(|cfg|{ cfg.set_row_span(pos(1, 1), 2); cfg.set_column_span(pos(1, 1), 2); cfg.set_alignment_horizontal(Cell(1, 1), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Cell(1, 1), AlignmentVertical::Center); }) .build(), "+---+---+---+---+" "|0-0|0-1|0-2|0-3|" "+---+---+---+---+" "|1-0| 123 |1-3|" "| | 345 | |" "+---+ 555 +---+" "|2-0| 333 |2-3|" "+---+---+---+---+" "|3-0|3-1|3-2|3-3|" "+---+---+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_1, grid(4, 4) .change_cell(pos(0, 0), "123\n345\n555\n333") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 2); cfg.set_column_span(pos(0, 0), 2); cfg.set_alignment_horizontal(Cell(0, 0), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Cell(0, 0), AlignmentVertical::Center); }) .build(), "+---+---+---+---+" "| 123 |0-2|0-3|" "| 345 | | |" "+ 555 +---+---+" "| 333 |1-2|1-3|" "+---+---+---+---+" "|2-0|2-1|2-2|2-3|" "+---+---+---+---+" "|3-0|3-1|3-2|3-3|" "+---+---+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_2, grid(4, 4) .change_cell(pos(2, 0), "123\n345\n555\n333") .config(|cfg|{ cfg.set_row_span(pos(2, 0), 2); cfg.set_column_span(pos(2, 0), 2); cfg.set_alignment_horizontal(Cell(2, 0), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Cell(2, 0), AlignmentVertical::Center); }) .build(), "+---+---+---+---+" "|0-0|0-1|0-2|0-3|" "+---+---+---+---+" "|1-0|1-1|1-2|1-3|" "+---+---+---+---+" "| 123 |2-2|2-3|" "| 345 | | |" "+ 555 +---+---+" "| 333 |3-2|3-3|" "+---+---+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_3, grid(4, 4) .change_cell(pos(2, 2), "123\n345\n555\n333") .config(|cfg|{ cfg.set_row_span(pos(2, 2), 2); cfg.set_column_span(pos(2, 2), 2); cfg.set_alignment_horizontal(Cell(2, 2), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Cell(2, 2), AlignmentVertical::Center); }) .build(), "+---+---+---+---+" "|0-0|0-1|0-2|0-3|" "+---+---+---+---+" "|1-0|1-1|1-2|1-3|" "+---+---+---+---+" "|2-0|2-1| 123 |" "| | | 345 |" "+---+---+ 555 +" "|3-0|3-1| 333 |" "+---+---+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_4, grid(4, 4) .change_cell(pos(0, 2), "123\n345\n555\n333") .config(|cfg|{ cfg.set_row_span(pos(0, 2), 2); cfg.set_column_span(pos(0, 2), 2); cfg.set_alignment_horizontal(Cell(0, 2), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Cell(0, 2), AlignmentVertical::Center); }) .build(), "+---+---+---+---+" "|0-0|0-1| 123 |" "| | | 345 |" "+---+---+ 555 +" "|1-0|1-1| 333 |" "+---+---+---+---+" "|2-0|2-1|2-2|2-3|" "+---+---+---+---+" "|3-0|3-1|3-2|3-3|" "+---+---+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_5, grid(4, 4) .change_cell(pos(0, 1), "123\n345\n555\n333") .config(|cfg|{ cfg.set_row_span(pos(0, 1), 2); cfg.set_column_span(pos(0, 1), 2); cfg.set_alignment_horizontal(Cell(0, 1), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Cell(0, 1), AlignmentVertical::Center); }) .build(), "+---+---+---+---+" "|0-0| 123 |0-3|" "| | 345 | |" "+---+ 555 +---+" "|1-0| 333 |1-3|" "+---+---+---+---+" "|2-0|2-1|2-2|2-3|" "+---+---+---+---+" "|3-0|3-1|3-2|3-3|" "+---+---+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_6, grid(4, 4) .change_cell(pos(1, 1), "123\n345\n555\n333") .config(|cfg|{ cfg.set_row_span(pos(1, 1), 3); cfg.set_column_span(pos(1, 1), 3); cfg.set_alignment_horizontal(Cell(1, 1), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Cell(1, 1), AlignmentVertical::Center); }) .build(), "+---+---+---+---+" "|0-0|0-1|0-2|0-3|" "+---+---+---+---+" "|1-0| 123 |" "+---+ 345 +" "|2-0| 555 |" "+---+ 333 +" "|3-0| |" "+---+---+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_7, grid(4, 4) .change_cell(pos(0, 0), "123\n345\n555\n333") .config(|cfg|{ cfg.set_row_span(pos(0, 0), 3); cfg.set_column_span(pos(0, 0), 3); cfg.set_alignment_horizontal(Cell(0, 0), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Cell(0, 0), AlignmentVertical::Center); }) .build(), "+---+---+---+---+" "| 123 |0-3|" "+ 345 +---+" "| 555 |1-3|" "+ 333 +---+" "| |2-3|" "+---+---+---+---+" "|3-0|3-1|3-2|3-3|" "+---+---+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_8, grid(4, 4) .change_cell(pos(0, 1), "123\n345\n555\n333") .config(|cfg|{ cfg.set_row_span(pos(0, 1), 3); cfg.set_column_span(pos(0, 1), 3); cfg.set_alignment_horizontal(Cell(0, 1), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Cell(0, 1), AlignmentVertical::Center); }) .build(), "+---+---+---+---+" "|0-0| 123 |" "+---+ 345 +" "|1-0| 555 |" "+---+ 333 +" "|2-0| |" "+---+---+---+---+" "|3-0|3-1|3-2|3-3|" "+---+---+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_9, grid(4, 4) .change_cell(pos(1, 0), "123\n345\n555\n333") .config(|cfg|{ cfg.set_row_span(pos(1, 0), 3); cfg.set_column_span(pos(1, 0), 3); cfg.set_alignment_horizontal(Cell(1, 0), AlignmentHorizontal::Center); cfg.set_alignment_vertical(Cell(1, 0), AlignmentVertical::Center); }) .build(), "+---+---+---+---+" "|0-0|0-1|0-2|0-3|" "+---+---+---+---+" "| 123 |1-3|" "+ 345 +---+" "| 555 |2-3|" "+ 333 +---+" "| |3-3|" "+---+---+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_10, grid(4, 4) .change_cell(pos(0, 0), "hello\nworld\n!\n!\n!\n!") .config(|cfg|{ cfg.set_column_span(pos(1, 1), 2); cfg.set_column_span(pos(3, 0), 3); cfg.set_row_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 3), 3); }) .build(), "+-----+---+---+---+" "|hello|0-1|0-2|0-3|" "|world| | | |" "|! | | | |" "+! +---+---+ +" "|! |1-1 | |" "|! | | |" "+-----+---+---+ +" "|2-0 |2-1|2-2| |" "+-----+---+---+---+" "|3-0 |3-3|" "+-----+---+---+---+" ); test_table!( _4x4_with_row_span_and_col_span_11, grid(4, 4) .change_cell(pos(0, 2), "q\nw\ne\nr\nt") .change_cell(pos(0, 3), "q1\nw1\ne1\nr1\nt1") .config(|cfg|{ cfg.set_column_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 2), 2); cfg.set_row_span(pos(0, 3), 3); }) .build(), "+---+---+---+---+" "|0-0 |q |q1 |" "| |w |w1 |" "+---+---+e +e1 +" "|1-0|1-1|r |r1 |" "| | |t |t1 |" "+---+---+---+ +" "|2-0|2-1|2-2| |" "+---+---+---+---+" "|3-0|3-1|3-2|3-3|" "+---+---+---+---+" ); test_table!( _3x5_with_row_span_and_col_span_12, grid(3, 5) .change_cell(pos(0, 3), "q\nw\ne\nr\nt") .change_cell(pos(0, 4), "q1\nw1\ne1\nr1\nt1") .config(|cfg|{ cfg.set_column_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 3), 2); cfg.set_row_span(pos(0, 4), 3); }) .build(), "+---+---+---+---+--+" "|0-0 |0-2|q |q1|" "| | |w |w1|" "+---+---+---+e +e1+" "|1-0|1-1|1-2|r |r1|" "| | | |t |t1|" "+---+---+---+---+ +" "|2-0|2-1|2-2|2-3| |" "+---+---+---+---+--+" ); test_table!( _3x5_with_row_span_and_col_span_13, grid(3, 5) .change_cell(pos(0, 3), "q\nw\ne\nr\nt\n") .change_cell(pos(0, 4), "q1\nw1\ne1\nr1\nt1\n") .config(|cfg|{ cfg.set_column_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 3), 2); cfg.set_row_span(pos(0, 4), 3); }) .build(), "+---+---+---+---+--+" "|0-0 |0-2|q |q1|" "| | |w |w1|" "| | |e |e1|" "+---+---+---+r +r1+" "|1-0|1-1|1-2|t |t1|" "| | | | | |" "+---+---+---+---+ +" "|2-0|2-1|2-2|2-3| |" "+---+---+---+---+--+" ); test_table!( _3x5_with_row_span_and_col_span_14, grid(3, 5) .change_cell(pos(0, 3), "q\nw\ne\nr\nt\n") .change_cell(pos(0, 4), "q1\nw1\ne1\nr1\nt1\n") .config(|cfg|{ cfg.set_column_span(pos(0, 0), 2); cfg.set_row_span(pos(0, 3), 2); cfg.set_row_span(pos(0, 4), 3); cfg.set_padding( Cell(0, 0), Sides::new(Indent::new(2, ' '), Indent::new(2, ' '), Indent::new(2, ' '), Indent::new(2, ' ')) ); cfg.set_padding( Cell(0, 3), Sides::new(Indent::new(2, ' '), Indent::new(2, ' '), Indent::new(2, ' '), Indent::new(2, ' ')) ); cfg.set_padding( Cell(1, 2), Sides::new(Indent::new(2, ' '), Indent::new(2, ' '), Indent::new(4, ' '), Indent::new(2, ' ')) ); }) .build(), "+---+---+-------+-----+--+" "| |0-2 | |q1|" "| | | |w1|" "| 0-0 | | q |e1|" "| | | w |r1|" "| | | e |t1|" "+---+---+-------+ r + +" "|1-0|1-1| | t | |" "| | | | | |" "| | | | | |" "| | | | | |" "| | | 1-2 | | |" "| | | | | |" "| | | | | |" "+---+---+-------+-----+ +" "|2-0|2-1|2-2 |2-3 | |" "+---+---+-------+-----+--+" ); // is this correct? test_table!( _3x4_with_row_span_and_col_span_13, grid(3, 5) .config(|cfg|{ cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 0), 2); cfg.set_row_span(pos(1, 0), 2); }) .build(), "+-+-+---+---+---+" "|0-0|0-2|0-3|0-4|" "+-+-+---+---+---+" "|1-0|1-2|1-3|1-4|" "+ +---+---+---+" "| |2-2|2-3|2-4|" "+-+-+---+---+---+" ); test_table!( _5x2_render_0, grid(5, 2) .change_cell(pos(1, 1), "1\n2\n3\n4") .config(|cfg|{ cfg.set_row_span(pos(1, 1), 4); }) .build(), "+---+---+" "|0-0|0-1|" "+---+---+" "|1-0|1 |" "+---+2 +" "|2-0|3 |" "+---+4 +" "|3-0| |" "+---+ +" "|4-0| |" "+---+---+" ); test_table!( _3x4_column_span_0, grid(3, 4) .config(|cfg|{ cfg.set_column_span(pos(0, 0), 2); cfg.set_column_span(pos(1, 0), 2); cfg.set_column_span(pos(2, 0), 2); }) .build(), "+-+-+---+---+" "|0-0|0-2|0-3|" "+-+-+---+---+" "|1-0|1-2|1-3|" "+-+-+---+---+" "|2-0|2-2|2-3|" "+-+-+---+---+" ); test_table!( _3x4_column_span_1, grid(3, 4) .config(|cfg|{ cfg.set_column_span(pos(0, 0), 3); cfg.set_column_span(pos(1, 0), 3); cfg.set_column_span(pos(2, 0), 3); }) .build(), "+-+++---+" "|0-0|0-3|" "+-+++---+" "|1-0|1-3|" "+-+++---+" "|2-0|2-3|" "+-+++---+" ); test_table!( _3x4_column_span_2, grid(3, 4) .change_cell(pos(0, 0), "") .change_cell(pos(1, 0), "") .change_cell(pos(2, 0), "") .config(|cfg|{ cfg.set_column_span(pos(0, 0), 3); cfg.set_column_span(pos(1, 0), 3); cfg.set_column_span(pos(2, 0), 3); }) .build(), "++++---+" "| |0-3|" "++++---+" "| |1-3|" "++++---+" "| |2-3|" "++++---+" ); // #[test] // #[ignore = "todo; create some logic of combining spans? or somehow resolving to not get the following"] // fn render_grid_with_row_3() { // let mut grid = util::new_grid::<3, 5>(); // grid.set(Entity::Cell(0, 0), Settings::new().span(2)); // grid.set(Entity::Cell(0, 1), Settings::new().span(2)); // grid.set(Entity::Cell(0, 2), Settings::new().span(2)); // assert_eq!( // grid.to_string(), // concat!( // "+---+---+---+---+--+\n", // "|0-0 |0-2|q |q1|\n", // "| | |w |w1|\n", // "+---+---+---+e +e1+\n", // "|1-0|1-1|1-2|r |r1|\n", // "| | | |t |t1|\n", // "+---+---+---+---+ +\n", // "|2-0|2-1|2-2|2-3| |\n", // "+---+---+---+---+--+\n", // ) // ); // } papergrid-0.14.0/tests/grid/settings.rs000064400000000000000000000202441046102023000161620ustar 00000000000000#![cfg(feature = "std")] use papergrid::ansi::ANSIBuf; use papergrid::config::{pos, AlignmentHorizontal, Border, Borders, Entity, Indent, Sides}; use crate::util::grid; use testing_table::test_table; test_table!( override_by_global_alignment_0, grid(2, 2) .data([["xxxxx", "xx"], ["y", "yyyyyyyyyy"]]) .config(|cfg| cfg.set_alignment_horizontal(Entity::Cell(0, 1), AlignmentHorizontal::Right)) .build(), "+-----+----------+" "|xxxxx| xx|" "+-----+----------+" "|y |yyyyyyyyyy|" "+-----+----------+" ); test_table!( override_by_global_alignment_1, grid(2, 2) .data([["xxxxx", "xx"], ["y", "yyyyyyyyyy"]]) .config(|cfg| cfg.set_alignment_horizontal(Entity::Global, AlignmentHorizontal::Center)) .build(), "+-----+----------+" "|xxxxx| xx |" "+-----+----------+" "| y |yyyyyyyyyy|" "+-----+----------+" ); test_table!( remove_border_test, grid(2, 2) .config(|cfg| { cfg.set_borders(Borders::default()); cfg.set_border( pos(0, 0), Border { top: Some('x'), bottom: Some('o'), left: Some('q'), ..Default::default() }, ); cfg.remove_border(pos(0, 0), (2, 2)); }) .build(), "0-00-1\n1-01-1" ); test_table!( entity_row_overrides_column_intersection_0, grid(2, 2) .config(|cfg| { cfg.set_borders(Borders::default()); cfg.set_padding( Entity::Column(0), Sides { bottom: Indent::new(3, '$'), ..Default::default() }, ); }) .build(), "0-00-1" "$$$ " "$$$ " "$$$ " "1-01-1" "$$$ " "$$$ " "$$$ " ); test_table!( entity_row_overrides_column_intersection_1, grid(2, 2) .config(|cfg| { cfg.set_borders(Borders::default()); cfg.set_padding( Entity::Column(0), Sides { bottom: Indent::new(3, '$'), ..Default::default() }, ); cfg.set_padding( Entity::Row(1), Sides { bottom: Indent::new(2, '#'), ..Default::default() }, ); }) .build(), "0-00-1" "$$$ " "$$$ " "$$$ " "1-01-1" "######" "######" ); test_table!( entity_column_overrides_row_intersection_0, grid(2, 2) .config(|cfg| { cfg.set_borders(Borders::default()); cfg.set_padding( Entity::Row(0), Sides { bottom: Indent::new(3, '$'), ..Default::default() }, ); }) .build(), "0-00-1\n$$$$$$\n$$$$$$\n$$$$$$\n1-01-1" ); test_table!( entity_column_overrides_row_intersection_1, grid(2, 2) .config(|cfg| { cfg.set_borders(Borders::default()); cfg.set_padding( Entity::Row(0), Sides::new( Indent::default(), Indent::default(), Indent::default(), Indent::new(3, '$'), ), ); cfg.set_padding( Entity::Column(1), Sides::new( Indent::default(), Indent::default(), Indent::default(), Indent::new(2, '#'), ), ); }) .build(), "0-00-1\n$$$###\n$$$###\n$$$###\n1-01-1\n ###\n ###" ); test_table!( test_justification_char_left_alignment, grid(2, 2) .data([["Hello", "World"], ["", "Hello Hello Hello Hello Hello"]]) .config(|cfg| cfg.set_justification(Entity::Global, '$')) .build(), "+-----+-----------------------------+" "|Hello|World$$$$$$$$$$$$$$$$$$$$$$$$|" "+-----+-----------------------------+" "|$$$$$|Hello Hello Hello Hello Hello|" "+-----+-----------------------------+" ); test_table!( test_justification_char_right_alignment, grid(2, 2) .data([["Hello", "World"], ["", "Hello Hello Hello Hello Hello"]]) .config(|cfg| { cfg.set_justification(Entity::Global, '$'); cfg.set_alignment_horizontal(Entity::Global, AlignmentHorizontal::Right); }) .build(), "+-----+-----------------------------+" "|Hello|$$$$$$$$$$$$$$$$$$$$$$$$World|" "+-----+-----------------------------+" "|$$$$$|Hello Hello Hello Hello Hello|" "+-----+-----------------------------+" ); test_table!( test_justification_char_center_alignment, grid(2, 2) .data([["Hello", "World"], ["", "Hello Hello Hello Hello Hello"]]) .config(|cfg| { cfg.set_justification(Entity::Global, '$'); cfg.set_alignment_horizontal(Entity::Global, AlignmentHorizontal::Center); }) .build(), "+-----+-----------------------------+" "|Hello|$$$$$$$$$$$$World$$$$$$$$$$$$|" "+-----+-----------------------------+" "|$$$$$|Hello Hello Hello Hello Hello|" "+-----+-----------------------------+" ); test_table!( test_justification_color_left_alignment, grid(2, 2) .data([["Hello", "World"], ["", "Hello Hello Hello Hello Hello"]]) .config(|cfg| { cfg.set_justification(Entity::Global, '$'); cfg.set_justification_color(Entity::Global, Some(ANSIBuf::new("\u{1b}[34m", "\u{1b}[39m"))); }) .build(), "+-----+-----------------------------+" "|Hello|World\u{1b}[34m$$$$$$$$$$$$$$$$$$$$$$$$\u{1b}[39m|" "+-----+-----------------------------+" "|\u{1b}[34m$$$$$\u{1b}[39m|Hello Hello Hello Hello Hello|" "+-----+-----------------------------+" ); test_table!( test_justification_color_right_alignment, grid(2, 2) .data([["Hello", "World"], ["", "Hello Hello Hello Hello Hello"]]) .config(|cfg| { cfg.set_justification(Entity::Global, '$'); cfg.set_justification_color(Entity::Global, Some(ANSIBuf::new("\u{1b}[34m", "\u{1b}[39m"))); cfg.set_alignment_horizontal(Entity::Global, AlignmentHorizontal::Right); }) .build(), "+-----+-----------------------------+" "|Hello|\u{1b}[34m$$$$$$$$$$$$$$$$$$$$$$$$\u{1b}[39mWorld|" "+-----+-----------------------------+" "|\u{1b}[34m$$$$$\u{1b}[39m|Hello Hello Hello Hello Hello|" "+-----+-----------------------------+" ); test_table!( test_justification_color_center_alignment, grid(2, 2) .data([["Hello", "World"], ["", "Hello Hello Hello Hello Hello"]]) .config(|cfg| { cfg.set_justification(Entity::Global, '$'); cfg.set_justification_color(Entity::Global, Some(ANSIBuf::new("\u{1b}[34m", "\u{1b}[39m"))); cfg.set_alignment_horizontal(Entity::Global, AlignmentHorizontal::Center); }) .build(), "+-----+-----------------------------+" "|Hello|\u{1b}[34m$$$$$$$$$$$$\u{1b}[39mWorld\u{1b}[34m$$$$$$$$$$$$\u{1b}[39m|" "+-----+-----------------------------+" "|\u{1b}[34m$$\u{1b}[39m\u{1b}[34m$$$\u{1b}[39m|Hello Hello Hello Hello Hello|" "+-----+-----------------------------+" ); test_table!( test_justification_color_center_alignment_entity, grid(2, 2) .data([["Hello", "World"], ["", "Hello Hello Hello Hello Hello"]]) .config(|cfg| { cfg.set_justification(Entity::Cell(0, 0), '$'); cfg.set_justification_color(Entity::Column(1), Some(ANSIBuf::new("\u{1b}[34m", "\u{1b}[39m"))); cfg.set_alignment_horizontal(Entity::Row(2), AlignmentHorizontal::Center); }) .build(), "+-----+-----------------------------+" "|Hello|World\u{1b}[34m \u{1b}[39m|" "+-----+-----------------------------+" "| |Hello Hello Hello Hello Hello|" "+-----+-----------------------------+" ); papergrid-0.14.0/tests/grid/styling.rs000064400000000000000000000244421046102023000160170ustar 00000000000000#![cfg(feature = "std")] use papergrid::config::{pos, AlignmentHorizontal, Border, Borders, Entity, Indent, Sides}; use testing_table::test_table; use crate::util::grid; #[cfg(feature = "ansi")] use ::{papergrid::ansi::ANSIBuf, std::convert::TryFrom}; test_table!( grid_2x2_custom_frame_test, grid(2, 2) .config(|cfg| (0..2).for_each(|r| (0..2).for_each(|c| cfg.set_border(pos(r, c), Border::full('*', '*', '|', '|', '#', '#', '#', '#'))))) .build(), "#***#***#" "|0-0|0-1|" "#***#***#" "|1-0|1-1|" "#***#***#" ); test_table!( grid_2x2_custom_column_test_0, grid(2, 2) .config(|cfg| (0..2).for_each(|r| cfg.set_border(pos(r, 1), Border::full('*', '*', '|', '|', '#', '#', '#', '#')))) .build(), "+---#***#" "|0-0|0-1|" "+---#***#" "|1-0|1-1|" "+---#***#" ); test_table!( grid_2x2_custom_column_test_1, grid(2, 2) .config(|cfg| (0..2).for_each(|r| cfg.set_border(pos(r, 0), Border::full('*', '*', '|', '|', '#', '#', '#', '#')))) .build(), "#***#---+" "|0-0|0-1|" "#***#---+" "|1-0|1-1|" "#***#---+" ); test_table!( grid_2x2_custom_row_test_0, grid(2, 2) .config(|cfg| (0..2).for_each(|c| cfg.set_border(pos(0, c), Border::full('*', '*', '|', '|', '#', '#', '#', '#')))) .build(), "#***#***#" "|0-0|0-1|" "#***#***#" "|1-0|1-1|" "+---+---+" ); test_table!( grid_2x2_custom_row_test_1, grid(2, 2) .config(|cfg| (0..2).for_each(|c| cfg.set_border(pos(1, c), Border::full('*', '*', '|', '|', '#', '#', '#', '#')))) .build(), "+---+---+" "|0-0|0-1|" "#***#***#" "|1-0|1-1|" "#***#***#" ); test_table!( grid_2x2_change_cell_border_test_0, grid(2, 2) .config(|cfg| (0..2).for_each(|_| cfg.set_border(pos(0, 1), Border::full('*', '^', '@', '#', '~', '!', '%', '&')))) .build(), "+---~***!" "|0-0@0-1#" "+---%^^^&" "|1-0|1-1|" "+---+---+" ); test_table!( grid_2x2_alignment_test_0, grid(2, 2) .change_cell(pos(0, 0), "asd ") .change_cell(pos(0, 1), "asd ") .config(|cfg| { cfg.set_alignment_horizontal(Entity::Column(0), AlignmentHorizontal::Left); cfg.set_alignment_horizontal(Entity::Column(1), AlignmentHorizontal::Right); }) .build(), "+-------+-------+" "|asd |asd |" "+-------+-------+" "|1-0 | 1-1|" "+-------+-------+" ); test_table!( grid_2x2_alignment_test_1, grid(2, 2) .data([["asd ", "asd "], ["asd ", "asd "]]) .config(|cfg| { cfg.set_alignment_horizontal(Entity::Column(0), AlignmentHorizontal::Left); cfg.set_alignment_horizontal(Entity::Column(1), AlignmentHorizontal::Right); }) .build(), "+-------+-------+" "|asd |asd |" "+-------+-------+" "|asd |asd |" "+-------+-------+" ); test_table!( grid_2x2_indent_test, grid(2, 2) .config(|cfg| { cfg.set_padding( Entity::Global, Sides::new( Indent::spaced(1), Indent::spaced(1), Indent::spaced(1), Indent::spaced(1), ), ); cfg.set_padding(Entity::Column(0), Sides::new( Indent::default(), Indent::default(), Indent::default(), Indent::default(), )); }) .build(), "+---+-----+" "|0-0| |" "| | 0-1 |" "| | |" "+---+-----+" "|1-0| |" "| | 1-1 |" "| | |" "+---+-----+" ); test_table!( grid_2x2_vertical_resize_test, grid(2, 2).change_cell(pos(1, 1), "asd ").build(), "+---+--------+" "|0-0|0-1 |" "+---+--------+" "|1-0|asd |" "+---+--------+" ); test_table!( grid_2x2_without_frame_test_0, grid(2, 2) .config(|cfg| { cfg.set_borders(Borders { vertical: Some(' '), ..Default::default() }); }) .build(), "0-0 0-1" "1-0 1-1" ); test_table!( grid_2x2_without_frame_test_1, grid(2, 2) .config(|cfg| { cfg.set_borders(Borders { vertical: Some(' '), horizontal: Some(' '), intersection: Some(' '), ..Default::default() }); }) .build(), "0-0 0-1" " " "1-0 1-1" ); test_table!( grid_2x2_custom_border_test, grid(2, 2) .config(|cfg| { cfg.set_border( pos(0, 0), Border { bottom: Some('-'), top: Some('*'), left: Some('$'), left_top_corner: Some(' '), left_bottom_corner: Some('+'), ..Default::default() }, ); cfg.set_border( pos(0, 1), Border::full('*', '-', '@', '%', ' ', ' ', '+', '+'), ); cfg.set_border( pos(1, 0), Border { bottom: Some('*'), left: Some('#'), left_top_corner: Some('+'), left_bottom_corner: Some('\u{0020}'), ..Default::default() }, ); cfg.set_border( pos(1, 1), Border { bottom: Some('*'), left: Some('^'), left_top_corner: Some('+'), right_top_corner: Some('+'), right: Some('!'), left_bottom_corner: Some(' '), right_bottom_corner: Some(' '), ..Default::default() }, ); }) .build(), " *** *** " "$0-0@0-1%" "+---+---+" "#1-0^1-1!" "\u{0020}*** *** " ); test_table!( when_border_is_not_complete_default_char_is_used_test, grid(2, 2) .config(|cfg| { cfg.set_borders(Borders { vertical: Some(' '), ..Default::default() }); cfg.set_border( pos(1, 1), Border { top: Some('*'), ..Default::default() }, ); }) .build(), "0-0 0-1" " ***" "1-0 1-1" ); test_table!( when_1_vertical_is_set_second_must_use_default_test, grid(2, 2) .config(|cfg| { cfg.set_borders(Borders::default()); cfg.set_border( pos(1, 0), Border { right: Some('*'), ..Default::default() }, ); }) .build(), "0-0 0-1" "1-0*1-1" ); #[cfg(feature = "ansi")] test_table!( grid_2x2_ansi_border_test, grid(2, 2) .config(|cfg| { (0..2).for_each(|r| (0..2).for_each(|c| { let border = Border::full('*', '#', '~', '!', '@', '$', '%', '^'); let color = Border::full( ANSIBuf::new("\u{1b}[32m\u{1b}[41m", "\u{1b}[39m\u{1b}[49m"), ANSIBuf::new("\u{1b}[34m\u{1b}[42m", "\u{1b}[39m\u{1b}[49m"), ANSIBuf::new("\u{1b}[37m\u{1b}[41m", "\u{1b}[39m\u{1b}[49m"), ANSIBuf::new("\u{1b}[37m\u{1b}[41m", "\u{1b}[39m\u{1b}[49m"), ANSIBuf::new("\u{1b}[35m", "\u{1b}[39m"), ANSIBuf::new("\u{1b}[44m", "\u{1b}[49m"), ANSIBuf::new("\u{1b}[33m", "\u{1b}[39m"), ANSIBuf::new("\u{1b}[43m", "\u{1b}[49m"), ); let pos = (r, c).into(); cfg.set_border(pos, border); cfg.set_border_color(pos, color); })) }) .build(), "\u{1b}[35m@\u{1b}[39m\u{1b}[32m\u{1b}[41m***\u{1b}[39m\u{1b}[49m\u{1b}[35m@\u{1b}[39m\u{1b}[32m\u{1b}[41m***\u{1b}[39m\u{1b}[49m\u{1b}[44m$\u{1b}[49m\n\u{1b}[37m\u{1b}[41m~\u{1b}[39m\u{1b}[49m0-0\u{1b}[37m\u{1b}[41m~\u{1b}[39m\u{1b}[49m0-1\u{1b}[37m\u{1b}[41m!\u{1b}[39m\u{1b}[49m\n\u{1b}[35m@\u{1b}[39m\u{1b}[32m\u{1b}[41m***\u{1b}[39m\u{1b}[49m\u{1b}[35m@\u{1b}[39m\u{1b}[32m\u{1b}[41m***\u{1b}[39m\u{1b}[49m\u{1b}[44m$\u{1b}[49m\n\u{1b}[37m\u{1b}[41m~\u{1b}[39m\u{1b}[49m1-0\u{1b}[37m\u{1b}[41m~\u{1b}[39m\u{1b}[49m1-1\u{1b}[37m\u{1b}[41m!\u{1b}[39m\u{1b}[49m\n\u{1b}[33m%\u{1b}[39m\u{1b}[34m\u{1b}[42m###\u{1b}[39m\u{1b}[49m\u{1b}[33m%\u{1b}[39m\u{1b}[34m\u{1b}[42m###\u{1b}[39m\u{1b}[49m\u{1b}[43m^\u{1b}[49m" ); #[cfg(feature = "ansi")] test_table!( grid_2x2_ansi_global_set_test, grid(2, 2) .config(|cfg| { let color = "\u{1b}[1m\u{1b}[31m\u{1b}[44m \u{1b}[0m"; cfg.set_border_color_default(ANSIBuf::try_from(color).unwrap()); }) .build(), "\u{1b}[1m\u{1b}[31m\u{1b}[44m+---+---+\u{1b}[22m\u{1b}[39m\u{1b}[49m" "\u{1b}[1m\u{1b}[31m\u{1b}[44m|\u{1b}[22m\u{1b}[39m\u{1b}[49m0-0\u{1b}[1m\u{1b}[31m\u{1b}[44m|\u{1b}[22m\u{1b}[39m\u{1b}[49m0-1\u{1b}[1m\u{1b}[31m\u{1b}[44m|\u{1b}[22m\u{1b}[39m\u{1b}[49m" "\u{1b}[1m\u{1b}[31m\u{1b}[44m+---+---+\u{1b}[22m\u{1b}[39m\u{1b}[49m" "\u{1b}[1m\u{1b}[31m\u{1b}[44m|\u{1b}[22m\u{1b}[39m\u{1b}[49m1-0\u{1b}[1m\u{1b}[31m\u{1b}[44m|\u{1b}[22m\u{1b}[39m\u{1b}[49m1-1\u{1b}[1m\u{1b}[31m\u{1b}[44m|\u{1b}[22m\u{1b}[39m\u{1b}[49m" "\u{1b}[1m\u{1b}[31m\u{1b}[44m+---+---+\u{1b}[22m\u{1b}[39m\u{1b}[49m" ); #[cfg(feature = "ansi")] #[test] fn grid_2x2_ansi_border_none_if_string_is_not_1_char_test() { assert!(ANSIBuf::try_from("12").is_ok()); assert!(ANSIBuf::try_from("123").is_ok()); assert!(ANSIBuf::try_from("1\n2").is_ok()); assert!(ANSIBuf::try_from("1\n2\n").is_ok()); assert!(ANSIBuf::try_from("\n1\n2").is_ok()); assert!(ANSIBuf::try_from("\n1\n2\n").is_ok()); assert!(ANSIBuf::try_from("").is_err()); assert!( ANSIBuf::try_from("\u{1b}[1m\u{1b}[31m\u{1b}[44m1\u{1b}[22m\u{1b}[39m\u{1b}[49m").is_ok() ); assert!(ANSIBuf::try_from("\u{1b}[1m\u{1b}[31m\u{1b}[44m1\u{1b}[0m").is_ok()); assert!(ANSIBuf::try_from("\u{1b}[1;31;44m1\u{1b}[22m\u{1b}[39m\u{1b}[49m").is_ok()); assert!(ANSIBuf::try_from("\u{1b}[1;31;44m1\u{1b}[0m").is_ok()); } papergrid-0.14.0/tests/main.rs000064400000000000000000000000251046102023000143140ustar 00000000000000mod grid; mod util; papergrid-0.14.0/tests/util/grid_builder.rs000064400000000000000000000066141046102023000170120ustar 00000000000000#![cfg(feature = "std")] #![allow(unused_macros, unused_imports)] use std::collections::HashMap; use papergrid::{ colors::NoColors, config::{pos, spanned::SpannedConfig, Borders, Position}, dimension::{spanned::SpannedGridDimension, Dimension, Estimate}, grid::iterable::Grid, records::{IterRecords, Records}, }; pub fn grid(rows: usize, cols: usize) -> GridBuilder { GridBuilder::new(rows, cols) } #[derive(Debug, Default, Clone)] pub struct GridBuilder { size: (usize, usize), cfg: SpannedConfig, data: HashMap, } impl GridBuilder { pub fn new(rows: usize, cols: usize) -> Self { let mut cfg = SpannedConfig::default(); cfg.set_borders(DEFAULT_BORDERS); Self { size: (rows, cols), cfg, ..Default::default() } } pub fn config(mut self, mut f: impl FnMut(&mut SpannedConfig)) -> Self { f(&mut self.cfg); self } pub fn data( mut self, data: impl IntoIterator>>, ) -> Self { for (i, rows) in data.into_iter().enumerate() { for (j, text) in rows.into_iter().enumerate() { let text = text.into(); self.data.insert(pos(i, j), text); } } self } pub fn change_cell(mut self, pos: Position, text: impl Into) -> Self { self.data.insert(pos, text.into()); self } pub fn build(self) -> String { let mut data = records(self.size.0, self.size.1); for (p, text) in self.data { data[p.row()][p.col()] = text; } let grid = build_grid(data, self.cfg, self.size); grid.to_string() } } fn build_grid( data: Vec>, cfg: SpannedConfig, shape: (usize, usize), ) -> Grid>>, SpannedGridDimension, SpannedConfig, NoColors> { let records = IterRecords::new(data, shape.1, Some(shape.0)); let mut dims = SpannedGridDimension::default(); dims.estimate(&records, &cfg); Grid::new(records, dims, cfg, NoColors) } fn records(rows: usize, cols: usize) -> Vec> { let mut records = vec![vec![String::new(); cols]; rows]; (0..rows).for_each(|row| { (0..cols).for_each(|col| { let text = format!("{row}-{col}"); records[row][col] = text; }); }); records } pub const DEFAULT_BORDERS: Borders = Borders { top: Some('-'), top_left: Some('+'), top_right: Some('+'), top_intersection: Some('+'), bottom: Some('-'), bottom_left: Some('+'), bottom_right: Some('+'), bottom_intersection: Some('+'), horizontal: Some('-'), left_intersection: Some('+'), right_intersection: Some('+'), left: Some('|'), right: Some('|'), vertical: Some('|'), intersection: Some('+'), }; /// A [`Estimate`]or of a width for a [`Grid`]. /// /// [`Grid`]: crate::grid::iterable::Grid #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct ConstantDimension(pub Vec, pub Vec); impl Dimension for ConstantDimension { fn get_width(&self, column: usize) -> usize { self.0[column] } fn get_height(&self, row: usize) -> usize { self.1[row] } } impl Estimate for ConstantDimension { fn estimate(&mut self, _: R, _: &SpannedConfig) {} } papergrid-0.14.0/tests/util/mod.rs000064400000000000000000000001041046102023000151220ustar 00000000000000mod grid_builder; #[cfg(feature = "std")] pub use grid_builder::*;