blob: 3702f13260b1292704857b676eda4496cddb073b [file] [log] [blame]
use crate::utils::unicode_str_width;
use itertools::{EitherOrBoth, Itertools};
use pulldown_cmark::escape::StrWrite;
use pulldown_cmark::Alignment;
use unicode_segmentation::UnicodeSegmentation;
use std::borrow::Cow;
pub(super) struct TableState<'a> {
/// Alignment markers for HTML rendering
/// * :-: center alignment
/// * :-- left alignment
/// * --: right alignmet
/// * --- no alignmet
alignment: Vec<Alignment>,
/// Table headers
headers: Vec<Cow<'a, str>>,
/// Keep track of the widest cell in each column.
/// Will use this info to align columns when rewriting
max_column_width: Vec<usize>,
/// Table rows
body: Vec<Vec<Cow<'a, str>>>,
/// Keep track of whether or not we're writing to the headers
write_to_body: bool,
/// Keep track of Which cell we're currently operating on.
col_index: usize,
}
impl<'a> TableState<'a> {
pub(super) fn new(alignment: Vec<Alignment>) -> Self {
let capacity = alignment.len();
Self {
alignment,
headers: Vec::with_capacity(capacity),
max_column_width: vec![3; capacity],
body: vec![],
write_to_body: false,
col_index: 0,
}
}
/// Write some values to the table state.
pub(super) fn write(&mut self, value: Cow<'a, str>) {
if self.write_to_body {
// push or update the body
self.write_cell(value)
} else {
// push or update the headers
self.write_header(value)
}
}
/// Allows the caller to advance the internal col_index.
/// All subsequent calls to `write` will affect the next cell
pub(super) fn increment_col_index(&mut self) {
self.col_index += 1;
}
/// Update the table state to start writing to the table body
pub(super) fn push_row(&mut self) {
self.body.push(Vec::with_capacity(self.alignment.len()));
self.write_to_body = true;
self.col_index = 0;
}
fn write_header(&mut self, text: Cow<'a, str>) {
let header_width = unicode_str_width(&text);
if let Some(column_header) = self.headers.get_mut(self.col_index) {
*column_header += text;
let current_width = self
.max_column_width
.get_mut(self.col_index)
.expect("should be set");
*current_width += header_width;
} else {
self.headers.push(text);
self.update_column_width(self.col_index, header_width);
}
}
fn write_cell(&mut self, text: Cow<'a, str>) {
let row = self
.body
.last_mut()
.expect("can only write cells after push_row called");
if let Some(cell_value) = row.get_mut(self.col_index) {
let current_width = unicode_str_width(cell_value) + unicode_str_width(&text);
*cell_value += text;
self.update_column_width(self.col_index, current_width);
} else {
let cell_width = unicode_str_width(&text);
row.push(text);
self.update_column_width(self.col_index, cell_width);
}
}
fn update_column_width(&mut self, index: usize, column_width: usize) {
if let Some(old_column_width) = self.max_column_width.get_mut(index) {
if *old_column_width < column_width {
*old_column_width = column_width
}
}
}
pub(super) fn format(self) -> std::io::Result<String> {
let mut result = String::new();
self.rewrite_header(&mut result)?;
self.rewrite_alignment(&mut result)?;
self.rewrite_body(&mut result)?;
Ok(result)
}
fn write_wth_padding(buffer: &mut String, value: &str, mut size: usize) -> std::io::Result<()> {
let offset = UnicodeSegmentation::graphemes(value, true).map(|grapheme| unicode_str_width(grapheme).saturating_sub(1)).sum();
size = size.saturating_sub(offset);
write!(buffer, " {value:<0$} |", size)
}
fn rewrite_header(&self, buffer: &mut String) -> std::io::Result<()> {
for (header, width) in self.headers.iter().zip(self.max_column_width.iter()) {
Self::write_wth_padding(buffer, header, *width)?;
}
Ok(())
}
fn rewrite_alignment(&self, buffer: &mut String) -> std::io::Result<()> {
buffer.push('\n');
for (alignment, width) in self.alignment.iter().zip(self.max_column_width.iter()) {
let alignment = match alignment {
Alignment::Center => {
// :-:
// (- 2 to account for 2 `:`)
format!(":{:-^1$}:", "-", width - 2)
}
Alignment::Left => {
// :--
format!("{:-<1$}", ":", width)
}
Alignment::Right => {
// --:
format!("{:->1$}", ":", width)
}
Alignment::None => {
// ---
"-".repeat(*width)
}
};
Self::write_wth_padding(buffer, &alignment, *width)?;
}
Ok(())
}
fn rewrite_body(&self, buffer: &mut String) -> std::io::Result<()> {
for row in self.body.iter() {
buffer.push('\n');
for either_or_both in row.iter().zip_longest(self.max_column_width.iter()) {
match either_or_both {
EitherOrBoth::Both(cell, width) => {
Self::write_wth_padding(buffer, cell, *width)?;
}
EitherOrBoth::Right(width) => {
Self::write_wth_padding(buffer, "", *width)?;
}
EitherOrBoth::Left(_) => {
// There may be fewer cells in a row, but there should never be more cells.
// See https://github.github.com/gfm/#example-204 and the preceding text
unreachable!()
}
}
}
}
Ok(())
}
}