use unicode_width::UnicodeWidthStr;
use crate::table::{format_separator_cells, is_delimiter_cell, split_cells};
mod row_parsing;
use row_parsing::{cell_is_semantically_empty, split_physical_rows};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Cell {
payload: String,
leading_empty: bool,
}
#[tracing::instrument(level = "trace", skip(trimmed))]
pub(crate) fn parse_rows(trimmed: &[String]) -> (Vec<Vec<Cell>>, bool) {
let physical_rows = trimmed
.iter()
.map(|line| parse_cells(line))
.collect::<Vec<_>>();
let (parsed_rows, split_within_line) = split_physical_rows(physical_rows);
let rows = parsed_rows
.into_iter()
.enumerate()
.filter_map(|(row_index, row)| retain_parsed_row(row_index, &row).then_some(row))
.collect();
(rows, split_within_line)
}
fn retain_parsed_row(row_index: usize, row: &[Cell]) -> bool {
if row.iter().all(cell_is_semantically_empty) {
tracing::debug!(
row_index,
cell_count = row.len(),
error_category = "empty_row_discarded",
"discarded empty parsed row"
);
false
} else {
tracing::debug!(row_index, cell_count = row.len(), "parsed table row");
true
}
}
pub(crate) fn clean_rows(rows: Vec<Vec<Cell>>) -> Vec<Vec<String>> {
rows.into_iter()
.map(|row| {
row.into_iter()
.map(|cell| cell.leading_empty.then(String::new).unwrap_or(cell.payload))
.collect::<Vec<_>>()
})
.filter(|row| row.iter().any(|cell| !cell.is_empty()))
.collect()
}
pub(crate) fn calculate_widths(rows: &[Vec<String>], max_cols: usize) -> Vec<usize> {
let mut widths = vec![0; max_cols];
for row in rows {
for (idx, cell) in row.iter().enumerate() {
widths[idx] = widths[idx].max(emitted_cell_width(cell));
}
}
widths
}
pub(crate) fn format_rows(rows: &[Vec<String>], widths: &[usize], indent: &str) -> Vec<String> {
rows.iter()
.map(|row| {
let padded: Vec<String> = row
.iter()
.enumerate()
.map(|(i, cell)| pad_cell_to_width(cell, widths[i]))
.collect();
format!("{}| {} |", indent, padded.join(" | "))
})
.collect()
}
pub(crate) fn insert_separator(
out: Vec<String>,
sep_cells: Option<Vec<String>>,
widths: &[usize],
indent: &str,
) -> Vec<String> {
if let Some(mut cells) = sep_cells {
while cells.len() < widths.len() {
cells.push(String::new());
}
let sep_padded = format_separator_cells(widths, &cells);
if sep_padded.is_empty() {
return out;
}
let sep_line_out = format!("{}| {} |", indent, sep_padded.join(" | "));
if let Some(first) = out.first().cloned() {
let mut with_sep = vec![first, sep_line_out];
with_sep.extend(out.into_iter().skip(1));
return with_sep;
}
return vec![sep_line_out];
}
out
}
pub(crate) fn detect_separator(
sep_line: Option<&String>,
rows: &[Vec<Cell>],
max_cols: usize,
) -> (Option<Vec<String>>, Option<usize>) {
let mut sep_cells: Option<Vec<String>> = sep_line.map(|l| split_cells(l));
let mut sep_row_idx: Option<usize> = None;
let sep_invalid = invalid_separator(sep_cells.as_ref(), max_cols);
if should_use_second_row_as_separator(sep_invalid, rows) {
sep_cells = Some(rows[1].iter().map(|cell| cell.payload.clone()).collect());
sep_row_idx = Some(1);
}
(sep_cells, sep_row_idx)
}
fn invalid_separator(sep_cells: Option<&Vec<String>>, max_cols: usize) -> bool {
match sep_cells {
Some(c) => c.len() != max_cols,
None => true,
}
}
fn should_use_second_row_as_separator(sep_invalid: bool, rows: &[Vec<Cell>]) -> bool {
sep_invalid && second_row_is_separator(rows)
}
fn second_row_is_separator(rows: &[Vec<Cell>]) -> bool {
rows.len() > 1 && rows[1].iter().all(|cell| is_delimiter_cell(&cell.payload))
}
fn parse_cells(line: &str) -> Vec<Cell> {
let cells = split_cells(line);
let leading_empty_cells = cells.iter().take_while(|cell| cell.is_empty()).count();
cells
.into_iter()
.enumerate()
.map(|(index, payload)| Cell {
payload,
leading_empty: index < leading_empty_cells,
})
.collect()
}
fn pad_cell_to_width(cell: &str, width: usize) -> String {
let escaped = escape_literal_pipes(cell);
let padding = width.saturating_sub(emitted_cell_width(cell));
format!("{escaped}{}", " ".repeat(padding))
}
fn escape_literal_pipes(cell: &str) -> String { cell.replace('|', r"\|") }
fn emitted_cell_width(cell: &str) -> usize {
UnicodeWidthStr::width(escape_literal_pipes(cell).as_str())
}
#[cfg(test)]
mod tests;