#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(super) struct DocTable {
pub rows: Vec<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TableShape {
pub rows: usize,
pub columns: usize,
pub cells: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CellMark {
Line,
EndOfCell,
EndOfRow,
}
impl DocTable {
pub fn from_units<'a>(units: impl IntoIterator<Item = (&'a str, CellMark)>) -> Self {
let mut table = DocTable::default();
let mut row: Vec<String> = Vec::new();
let mut cell: Vec<&str> = Vec::new();
let flush_cell = |cell: &mut Vec<&str>, row: &mut Vec<String>| {
let text = cell.join(" ").trim().to_string();
cell.clear();
row.push(text);
};
for (text, mark) in units {
let text = text.trim();
if !text.is_empty() {
cell.push(text);
}
match mark {
CellMark::Line => {}
CellMark::EndOfCell => flush_cell(&mut cell, &mut row),
CellMark::EndOfRow => {
if !cell.is_empty() {
flush_cell(&mut cell, &mut row);
}
table.rows.push(std::mem::take(&mut row));
}
}
}
if !cell.is_empty() {
flush_cell(&mut cell, &mut row);
}
if !row.is_empty() {
table.rows.push(row);
}
table
}
pub fn is_empty(&self) -> bool {
self.rows.iter().all(|r| r.iter().all(|c| c.is_empty()))
}
pub fn shape(&self) -> TableShape {
TableShape {
rows: self.rows.len(),
columns: self.rows.iter().map(Vec::len).max().unwrap_or(0),
cells: self.rows.iter().map(Vec::len).sum(),
}
}
pub fn to_markdown(&self) -> String {
let max_cols = self.rows.iter().map(Vec::len).max().unwrap_or(0);
if max_cols == 0 {
return String::new();
}
let header_count = usize::from(self.rows.len() > 1);
let mut out = String::new();
for (i, row) in self.rows.iter().enumerate() {
out.push('|');
for col in 0..max_cols {
out.push(' ');
out.push_str(&escape_cell(row.get(col).map(String::as_str).unwrap_or("")));
out.push_str(" |");
}
out.push('\n');
if i + 1 == header_count && header_count < self.rows.len() {
out.push('|');
for _ in 0..max_cols {
out.push_str(" --- |");
}
out.push('\n');
}
}
while out.ends_with('\n') {
out.pop();
}
out
}
}
fn escape_cell(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'|' => out.push_str("\\|"),
'\n' | '\r' => out.push(' '),
_ => out.push(ch),
}
}
out
}
#[cfg(test)]
mod tests {
use super::CellMark::*;
use super::*;
#[test]
fn cells_and_rows_are_cut_at_their_own_marks() {
let table = DocTable::from_units(vec![
("Symbol", EndOfCell),
("Quantity", EndOfCell),
("Conversion", EndOfCell),
("", EndOfRow),
("B", EndOfCell),
("magnetic flux density", EndOfCell),
("1 G = 10 T", EndOfCell),
("", EndOfRow),
]);
assert_eq!(
table.rows,
vec![
vec!["Symbol", "Quantity", "Conversion"],
vec!["B", "magnetic flux density", "1 G = 10 T"],
]
);
assert_eq!(
table.shape(),
TableShape {
rows: 2,
columns: 3,
cells: 6
}
);
}
#[test]
fn a_multi_paragraph_cell_stays_one_cell() {
let table = DocTable::from_units(vec![
("Conversion from Gaussian and", Line),
("CGS EMU to SI", EndOfCell),
("", EndOfRow),
]);
assert_eq!(
table.rows,
vec![vec!["Conversion from Gaussian and CGS EMU to SI"]]
);
}
#[test]
fn a_row_missing_its_final_mark_is_still_kept() {
let table = DocTable::from_units(vec![("a", EndOfCell), ("b", EndOfCell)]);
assert_eq!(table.rows, vec![vec!["a", "b"]]);
}
#[test]
fn short_rows_are_padded_and_only_the_header_gets_a_separator() {
let table = DocTable {
rows: vec![
vec!["h1".into(), "h2".into(), "h3".into()],
vec!["a".into()],
vec!["b".into(), "c".into()],
],
};
assert_eq!(
table.to_markdown(),
"| h1 | h2 | h3 |\n| --- | --- | --- |\n| a | | |\n| b | c | |"
);
}
#[test]
fn pipes_inside_a_cell_are_escaped() {
let table = DocTable {
rows: vec![vec!["a | b".into()]],
};
assert_eq!(table.to_markdown(), "| a \\| b |");
}
#[test]
fn a_table_of_empty_cells_reports_itself_empty() {
let table = DocTable::from_units(vec![("", EndOfCell), ("", EndOfRow)]);
assert!(table.is_empty());
}
}