use std::iter::repeat_n;
#[cfg(feature = "tty")]
use crossterm::style::{Stylize, style};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::content_split::{measure_text_width, split_line};
#[cfg(feature = "tty")]
use crate::style::{map_attribute, map_color};
use crate::{cell::Cell, row::Row, style::CellAlignment, table::Table, utils::ColumnDisplayInfo};
pub fn delimiter(cell: &Cell, info: &ColumnDisplayInfo, table: &Table) -> char {
if let Some(delimiter) = cell.delimiter {
delimiter
} else if let Some(delimiter) = info.delimiter {
delimiter
} else if let Some(delimiter) = table.delimiter {
delimiter
} else {
' '
}
}
pub fn format_content(table: &Table, display_info: &[ColumnDisplayInfo]) -> Vec<Vec<Vec<String>>> {
let mut table_content = Vec::with_capacity(table.rows.len() + 1);
if let Some(header) = table.header() {
table_content.push(format_row(header, display_info, table));
}
for row in table.rows.iter() {
table_content.push(format_row(row, display_info, table));
}
table_content
}
pub fn format_row(
row: &Row,
display_infos: &[ColumnDisplayInfo],
table: &Table,
) -> Vec<Vec<String>> {
let mut temp_row_content = Vec::with_capacity(display_infos.len());
let mut cell_iter = row.cells.iter();
for info in display_infos.iter() {
if info.is_hidden {
cell_iter.next();
continue;
}
let mut cell_lines = Vec::new();
let Some(cell) = cell_iter.next() else {
cell_lines.push(" ".repeat(info.width().into()));
temp_row_content.push(cell_lines);
continue;
};
let delimiter = delimiter(cell, info, table);
for line in cell.content.iter() {
if measure_text_width(line) > info.content_width.into() {
let mut parts = split_line(line, info, delimiter);
cell_lines.append(&mut parts);
} else {
cell_lines.push(line.into());
}
}
if let Some(lines) = row.max_height
&& cell_lines.len() > lines
{
let _ = cell_lines.split_off(lines);
let last_line = cell_lines
.get_mut(lines - 1)
.expect("We know it's this long.");
#[cfg(feature = "custom_styling")]
{
let stripped = console::strip_ansi_codes(last_line).to_string();
*last_line = stripped;
}
let max_width: usize = info.content_width.into();
let indicator = if table.truncation_indicator.width() > max_width {
""
} else {
table.truncation_indicator.as_str()
};
let indicator_width = indicator.width();
let mut truncate_at = 0;
let mut accumulated_width = indicator_width;
let mut full_string_fits = false;
let mut grapheme_iter = last_line.grapheme_indices(true).peekable();
while let Some((index, grapheme)) = grapheme_iter.next() {
truncate_at = index;
let new_width = accumulated_width + grapheme.width();
if new_width > max_width {
break;
}
accumulated_width += grapheme.width();
if grapheme_iter.peek().is_none() {
full_string_fits = true
}
}
if !full_string_fits {
let mut last_line_bytes = last_line.clone().into_bytes();
last_line_bytes.truncate(truncate_at);
let new_last_line =
String::from_utf8(last_line_bytes).expect("We cut at an exact char boundary");
*last_line = new_last_line;
}
last_line.push_str(indicator);
}
let cell_lines = cell_lines
.into_iter()
.map(|line| align_line(table, info, cell, line));
temp_row_content.push(cell_lines.collect());
}
let max_lines = temp_row_content.iter().map(Vec::len).max().unwrap_or(0);
let mut row_content = Vec::with_capacity(max_lines * display_infos.len());
let mut cell_line_iters: Vec<_> = temp_row_content.into_iter().map(Vec::into_iter).collect();
for _ in 0..max_lines {
let mut line = Vec::with_capacity(display_infos.len());
let mut cell_iter = cell_line_iters.iter_mut();
for info in display_infos.iter() {
if info.is_hidden {
continue;
}
let cell = cell_iter.next().unwrap();
match cell.next() {
Some(content) => line.push(content),
None => line.push(" ".repeat(info.width().into())),
}
}
row_content.push(line);
}
row_content
}
#[allow(unused_variables)]
fn align_line(table: &Table, info: &ColumnDisplayInfo, cell: &Cell, mut line: String) -> String {
let content_width = info.content_width;
let remaining: usize = usize::from(content_width).saturating_sub(measure_text_width(&line));
#[cfg(feature = "tty")]
if table.should_style() && table.style_text_only {
line = style_line(line, cell);
}
let alignment = if let Some(alignment) = cell.alignment {
alignment
} else if let Some(alignment) = info.cell_alignment {
alignment
} else {
CellAlignment::Left
};
let (left_pad, right_pad) = match alignment {
CellAlignment::Left => (0, remaining),
CellAlignment::Right => (remaining, 0),
CellAlignment::Center => (remaining.div_ceil(2), remaining / 2),
};
let left_spaces = usize::from(info.padding.0) + left_pad;
let right_spaces = right_pad + usize::from(info.padding.1);
let mut padded_line = String::with_capacity(left_spaces + line.len() + right_spaces);
padded_line.extend(repeat_n(' ', left_spaces));
padded_line.push_str(&line);
padded_line.extend(repeat_n(' ', right_spaces));
line = padded_line;
#[cfg(feature = "tty")]
if table.should_style() && !table.style_text_only {
return style_line(line, cell);
}
line
}
#[cfg(feature = "tty")]
fn style_line(line: String, cell: &Cell) -> String {
if cell.fg.is_none() && cell.bg.is_none() && cell.attributes.is_empty() {
return line;
}
let mut content = style(line);
if let Some(color) = cell.fg {
content = content.with(map_color(color));
}
if let Some(color) = cell.bg {
content = content.on(map_color(color));
}
for attribute in cell.attributes.iter() {
content = content.attribute(map_attribute(*attribute));
}
content.to_string()
}