use std::{
cmp,
iter::{repeat_n, FromIterator},
};
use ron::{Number, Value};
use tabled::{
builder::Builder,
grid::{
config::{
AlignmentHorizontal, AlignmentVertical, CompactMultilineConfig, Indent, Sides,
SpannedConfig,
},
dimension::{CompleteDimension, DimensionPriority, PoolTableDimension},
records::EmptyRecords,
util::string::{count_lines, get_line_width, get_lines, get_text_width},
},
settings::{style::Style, TableOption},
tables::{PoolTable, TableValue},
Table,
};
use crate::Orientation;
#[derive(Debug, Clone)]
pub struct RonTable {
cfg: CompactMultilineConfig,
plain: bool,
object_orientation: Orientation,
array_orientation: Orientation,
}
impl Default for RonTable {
fn default() -> Self {
Self {
plain: true,
cfg: configure_grid(),
array_orientation: Orientation::Column,
object_orientation: Orientation::Column,
}
}
}
impl RonTable {
pub fn new() -> Self {
Self::default()
}
pub fn collapse(&mut self) -> &mut Self {
self.plain = false;
self
}
pub fn map_orientation(&mut self, mode: Orientation) -> &mut Self {
self.object_orientation = mode;
self
}
pub fn seq_orientation(&mut self, mode: Orientation) -> &mut Self {
self.array_orientation = mode;
self
}
pub fn with<O>(&mut self, option: O) -> &mut Self
where
O: TableOption<EmptyRecords, CompactMultilineConfig, CompleteDimension>,
{
let mut records = EmptyRecords::default();
let mut dims = CompleteDimension::default();
option.change(&mut records, &mut self.cfg, &mut dims);
self
}
pub fn build(&self, value: &Value) -> String {
match self.plain {
true => plain_table(value, self),
false => collapsed_table(value, self),
}
}
}
fn configure_grid() -> CompactMultilineConfig {
let pad = Sides::new(
Indent::spaced(1),
Indent::spaced(1),
Indent::default(),
Indent::default(),
);
let mut cfg = CompactMultilineConfig::new();
cfg.set_padding(pad);
cfg.set_alignment_horizontal(AlignmentHorizontal::Left);
cfg.set_borders(tabled::grid::config::Borders::from(Style::ascii()));
cfg
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
struct CollapseCtx {
map_orientation: Orientation,
list_orientation: Orientation,
has_horizontal: bool,
has_vertical: bool,
alignment_horizontal: AlignmentHorizontal,
alignment_vertical: AlignmentVertical,
}
fn collapsed_table(value: &Value, cfg: &RonTable) -> String {
let ctx = CollapseCtx {
map_orientation: cfg.object_orientation,
list_orientation: cfg.array_orientation,
has_horizontal: cfg.cfg.get_borders().has_top(),
has_vertical: cfg.cfg.get_borders().has_left(),
alignment_horizontal: cfg.cfg.get_alignment_horizontal(),
alignment_vertical: cfg.cfg.get_alignment_vertical(),
};
let value = convert_value_to_table_value(value, ctx);
PoolTable::from(value)
.with(cfg.cfg)
.with(PoolTableDimension::new(
DimensionPriority::Last,
DimensionPriority::Last,
))
.to_string()
}
fn convert_value_to_table_value(value: &Value, ctx: CollapseCtx) -> TableValue {
match value {
Value::Map(map) => match ctx.map_orientation {
Orientation::Row => convert_map_to_row(map, ctx),
Orientation::Column => convert_map_to_column(map, ctx),
},
Value::Option(opt) => match opt {
Some(value) => convert_value_to_table_value(value, ctx),
None => TableValue::Cell(String::new()),
},
Value::Seq(list) => convert_list(list, ctx),
Value::Bool(value) => TableValue::Cell(value.to_string()),
Value::Char(char) => TableValue::Cell(char.to_string()),
Value::String(text) => TableValue::Cell(text.to_owned()),
Value::Unit => TableValue::Cell(String::new()),
Value::Bytes(bytes) => TableValue::Cell(format!("{:?}", bytes)),
Value::Number(num) => {
let value = match num {
Number::I8(num) => num.to_string(),
Number::I16(num) => num.to_string(),
Number::I32(num) => num.to_string(),
Number::I64(num) => num.to_string(),
Number::U8(num) => num.to_string(),
Number::U16(num) => num.to_string(),
Number::U32(num) => num.to_string(),
Number::U64(num) => num.to_string(),
Number::F32(num) => num.get().to_string(),
Number::F64(num) => num.get().to_string(),
_ => num.into_f64().to_string(),
};
TableValue::Cell(value)
}
}
}
fn convert_list(list: &[Value], ctx: CollapseCtx) -> TableValue {
let list = list
.iter()
.map(|value| convert_value_to_table_value(value, ctx))
.collect();
match ctx.list_orientation {
Orientation::Row => TableValue::Row(list),
Orientation::Column => TableValue::Column(list),
}
}
fn convert_map_to_column(map: &ron::Map, ctx: CollapseCtx) -> TableValue {
let mut keys = map
.keys()
.map(|key| convert_value_to_table_value(key, ctx))
.map(|key| {
let width = table_value_width(&key, ctx.has_vertical);
(key, width)
})
.collect::<Vec<_>>();
let key_width = keys.iter().map(|v| v.1).max().unwrap_or(0);
keys.iter_mut().for_each(|(key, width)| {
let left = key_width - *width;
if left > 0 {
table_value_increase_width(key, left, ctx.alignment_horizontal);
}
});
let data = keys
.into_iter()
.zip(map.values())
.map(|((key, _), value)| (key, convert_value_to_table_value(value, ctx)))
.map(|(key, value)| TableValue::Row(vec![key, value]))
.collect();
TableValue::Column(data)
}
fn convert_map_to_row(map: &ron::Map, ctx: CollapseCtx) -> TableValue {
let mut keys = map
.keys()
.map(|key| convert_value_to_table_value(key, ctx))
.map(|key| {
let height = table_value_height(&key, ctx.has_horizontal);
(key, height)
})
.collect::<Vec<_>>();
let key_height = keys.iter().map(|v| v.1).max().unwrap_or(0);
keys.iter_mut().for_each(|(key, width)| {
let left = key_height - *width;
if left > 0 {
table_value_increase_height(key, left, ctx.alignment_vertical);
}
});
let data = keys
.into_iter()
.zip(map.values())
.map(|((key, _), value)| (key, convert_value_to_table_value(value, ctx)))
.map(|(key, value)| TableValue::Column(vec![key, value]))
.collect();
TableValue::Row(data)
}
fn table_value_width(value: &TableValue, has_vertical: bool) -> usize {
match value {
TableValue::Row(list) => {
list.iter()
.map(|value| table_value_width(value, has_vertical))
.sum::<usize>()
+ (cmp::max(list.len(), 1) - 1) * has_vertical as usize
}
TableValue::Column(list) => list
.iter()
.map(|value| table_value_width(value, has_vertical))
.max()
.unwrap_or(0),
TableValue::Cell(string) => get_text_width(string),
}
}
fn table_value_height(value: &TableValue, has_horizontal: bool) -> usize {
match value {
TableValue::Row(list) => list
.iter()
.map(|value| table_value_width(value, has_horizontal))
.max()
.unwrap_or(0),
TableValue::Column(list) => {
list.iter()
.map(|value| table_value_height(value, has_horizontal))
.sum::<usize>()
+ (cmp::max(list.len(), 1) - 1) * has_horizontal as usize
}
TableValue::Cell(string) => count_lines(string),
}
}
fn table_value_increase_width(value: &mut TableValue, by: usize, ah: AlignmentHorizontal) {
match value {
TableValue::Row(list) => {
let mut left = by;
while left > 0 {
for value in list.iter_mut() {
left -= 1;
table_value_increase_width(value, 1, ah);
}
}
}
TableValue::Column(list) => {
for value in list.iter_mut() {
table_value_increase_width(value, by, ah);
}
}
TableValue::Cell(string) => *string = increase_string_width(string, by, ah),
}
}
fn table_value_increase_height(value: &mut TableValue, by: usize, av: AlignmentVertical) {
match value {
TableValue::Row(list) => {
for value in list.iter_mut() {
table_value_increase_height(value, by, av);
}
}
TableValue::Column(list) => {
let mut left = by;
while left > 0 {
for value in list.iter_mut() {
left -= 1;
table_value_increase_height(value, 1, av);
}
}
}
TableValue::Cell(string) => *string = increase_string_height(string, by, av),
}
}
fn increase_string_width(text: &str, by: usize, ah: AlignmentHorizontal) -> String {
let mut out = Vec::new();
for line in get_lines(text) {
let w = get_line_width(&line);
let (left, right) = indent_horizontal(ah, w + by, w);
let mut buf = String::new();
buf.extend(repeat_n(' ', left));
buf.push_str(&line);
buf.extend(repeat_n(' ', right));
out.push(buf);
}
out.join("\n")
}
fn increase_string_height(text: &str, by: usize, av: AlignmentVertical) -> String {
let mut out = Vec::new();
let count_lines = count_lines(text);
let (top, bottom) = indent_vertical(av, count_lines + by, count_lines);
out.extend(repeat_n(String::new(), top));
for line in get_lines(text) {
out.push(line.into_owned());
}
out.extend(repeat_n(String::new(), bottom));
out.join("\n")
}
fn indent_vertical(al: AlignmentVertical, available: usize, real: usize) -> (usize, usize) {
let top = indent_top(al, available, real);
let bottom = available - real - top;
(top, bottom)
}
fn indent_horizontal(al: AlignmentHorizontal, available: usize, real: usize) -> (usize, usize) {
let top = indent_left(al, available, real);
let right = available - real - top;
(top, right)
}
fn indent_top(al: AlignmentVertical, available: usize, real: usize) -> usize {
match al {
AlignmentVertical::Top => 0,
AlignmentVertical::Bottom => available - real,
AlignmentVertical::Center => (available - real) / 2,
}
}
fn indent_left(al: AlignmentHorizontal, available: usize, real: usize) -> usize {
match al {
AlignmentHorizontal::Left => 0,
AlignmentHorizontal::Right => available - real,
AlignmentHorizontal::Center => (available - real) / 2,
}
}
fn plain_table(value: &Value, cfg: &RonTable) -> String {
_plain_table(value, cfg, true)
}
fn _plain_table(value: &Value, cfg: &RonTable, outer: bool) -> String {
let config: SpannedConfig = cfg.cfg.into();
match value {
Value::Seq(arr) => match cfg.array_orientation {
Orientation::Column => seq_column_table(arr, cfg, &config),
Orientation::Row => seq_row_table(arr, cfg, &config),
},
Value::Map(map) => match cfg.object_orientation {
Orientation::Column => map_column_table(map, cfg, &config),
Orientation::Row => map_row_table(map, cfg, &config),
},
Value::Option(opt) => match opt {
Some(value) => _plain_table(value, cfg, outer),
None => String::new(),
},
Value::Unit => String::new(),
Value::String(text) => string_table(text.to_owned(), config, outer),
Value::Bool(val) => string_table(val.to_string(), config, outer),
Value::Char(char) => string_table(char.to_string(), config, outer),
Value::Bytes(bytes) => string_table(format!("{:?}", bytes), config, outer),
Value::Number(num) => {
let value = match num {
Number::I8(num) => num.to_string(),
Number::I16(num) => num.to_string(),
Number::I32(num) => num.to_string(),
Number::I64(num) => num.to_string(),
Number::U8(num) => num.to_string(),
Number::U16(num) => num.to_string(),
Number::U32(num) => num.to_string(),
Number::U64(num) => num.to_string(),
Number::F32(num) => num.get().to_string(),
Number::F64(num) => num.get().to_string(),
_ => num.into_f64().to_string(),
};
string_table(value, config, outer)
}
}
}
fn seq_column_table(arr: &Vec<Value>, cfg: &RonTable, config: &SpannedConfig) -> String {
let mut buf = Builder::with_capacity(1, 1);
for value in arr {
let val = _plain_table(value, cfg, false);
buf.push_record([val]);
}
buf.build().with(config).to_string()
}
fn seq_row_table(arr: &Vec<Value>, cfg: &RonTable, config: &SpannedConfig) -> String {
let mut buf = Vec::with_capacity(arr.len());
for value in arr {
let val = _plain_table(value, cfg, false);
buf.push(val);
}
Builder::from(vec![buf]).build().with(config).to_string()
}
fn map_column_table(map: &ron::Map, cfg: &RonTable, config: &SpannedConfig) -> String {
let mut buf = Builder::with_capacity(map.len(), 2);
for (key, value) in map.iter() {
let key = _plain_table(key, cfg, false);
let val = _plain_table(value, cfg, false);
buf.push_record([key, val]);
}
buf.build().with(config).to_string()
}
fn map_row_table(map: &ron::Map, cfg: &RonTable, config: &SpannedConfig) -> String {
let mut keys = Vec::with_capacity(map.len());
let mut vals = Vec::with_capacity(map.len());
for (key, value) in map.iter() {
let key = _plain_table(key, cfg, false);
let val = _plain_table(value, cfg, false);
vals.push(val);
keys.push(key);
}
Builder::from(vec![keys, vals])
.build()
.with(config)
.to_string()
}
fn string_table(val: String, config: SpannedConfig, outer: bool) -> String {
let mut table = Table::from_iter([[val]]);
table.with(config);
if !outer {
table.with(Style::empty());
}
table.to_string()
}