use crate::book::{Extra, Inline};
use serde::{Deserialize, Serialize};
pub const MAX_AUTO_COLUMNS: usize = 64;
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct Table {
#[serde(default)]
pub columns: Vec<TableColumn>,
#[serde(default, skip_serializing_if = "is_zero")]
pub header_rows: u32,
#[serde(default)]
pub rows: Vec<Vec<TableCell>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub caption: Option<Vec<Inline>>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct TableColumn {
#[serde(default)]
pub align: ColumnAlign,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TableCell {
#[serde(default)]
pub content: Vec<Inline>,
#[serde(default = "one", skip_serializing_if = "is_one")]
pub colspan: u32,
#[serde(default = "one", skip_serializing_if = "is_one")]
pub rowspan: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub align: Option<ColumnAlign>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ColumnAlign {
#[default]
Left,
Center,
Right,
}
fn is_zero(n: &u32) -> bool {
*n == 0
}
fn one() -> u32 {
1
}
fn is_one(n: &u32) -> bool {
*n == 1
}
impl TableColumn {
pub fn new(align: ColumnAlign) -> Self {
Self {
align,
extra: Extra::new(),
}
}
}
impl TableCell {
pub fn new(content: Vec<Inline>) -> Self {
Self {
content,
colspan: 1,
rowspan: 1,
align: None,
extra: Extra::new(),
}
}
pub fn text(s: impl Into<String>) -> Self {
Self::new(vec![Inline::text(s)])
}
pub fn with_span(mut self, colspan: u32, rowspan: u32) -> Self {
self.colspan = colspan;
self.rowspan = rowspan;
self
}
pub fn with_align(mut self, align: ColumnAlign) -> Self {
self.align = Some(align);
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedCell<'t> {
pub row: usize,
pub column: usize,
pub rowspan: usize,
pub colspan: usize,
pub align: ColumnAlign,
pub parts: Vec<&'t [Inline]>,
pub source: Option<(usize, usize)>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TableLayout<'t> {
pub columns: usize,
pub rows: usize,
pub header_rows: usize,
pub aligns: Vec<ColumnAlign>,
pub cells: Vec<PlacedCell<'t>>,
}
impl<'t> PlacedCell<'t> {
pub fn content(&self) -> Vec<Inline> {
let mut out = Vec::new();
for part in self.shown_parts() {
if !out.is_empty() {
out.push(Inline::LineBreak);
}
out.extend(part.iter().cloned());
}
out
}
pub fn shown_parts(&self) -> impl Iterator<Item = &'t [Inline]> + '_ {
self.parts
.iter()
.copied()
.filter(|p| !Inline::plain_text(p).trim().is_empty() || has_note(p))
}
pub fn is_blank(&self) -> bool {
self.parts
.iter()
.all(|p| Inline::plain_text(p).trim().is_empty() && !has_note(p))
}
}
impl Table {
pub fn new(columns: Vec<TableColumn>, header_rows: u32, rows: Vec<Vec<TableCell>>) -> Self {
Table {
columns,
header_rows,
rows,
caption: None,
extra: Extra::new(),
}
.normalized()
}
pub fn with_caption(mut self, caption: Vec<Inline>) -> Self {
self.caption = Some(caption);
self
}
fn width(&self) -> usize {
if !self.columns.is_empty() {
return self.columns.len();
}
let widest = self
.rows
.iter()
.map(|row| {
row.iter()
.fold(0usize, |sum, c| sum.saturating_add(span_of(c.colspan)))
})
.max()
.unwrap_or(0);
if self.rows.is_empty() {
0
} else {
widest.clamp(1, MAX_AUTO_COLUMNS)
}
}
pub fn layout(&self) -> TableLayout<'_> {
let rows = self.rows.len();
let width = self.width();
let header = (self.header_rows as usize).min(rows);
let aligns: Vec<ColumnAlign> = (0..width)
.map(|c| self.columns.get(c).map_or(ColumnAlign::Left, |c| c.align))
.collect();
let mut placed: Vec<PlacedCell> = Vec::new();
let mut covered = vec![0usize; width];
let mut owner: Vec<usize> = vec![0; width];
for (r, listed) in self.rows.iter().enumerate() {
if covered.iter().all(|&below| below > r) {
let mut cut: Vec<usize> = owner.clone();
cut.sort_unstable();
cut.dedup();
for i in cut {
let cell = &mut placed[i];
cell.rowspan = r - cell.row;
covered[cell.column..cell.column + cell.colspan].fill(r);
}
}
let mut next = 0;
let mut last: Option<usize> = None;
for (index, cell) in listed.iter().enumerate() {
let mut column = next;
while column < width && covered[column] > r {
column += 1;
}
if column >= width {
if let Some(to) = last
&& !cell.content.is_empty()
{
placed[to].parts.push(&cell.content);
}
continue;
}
let mut colspan = 1;
while colspan < span_of(cell.colspan)
&& column + colspan < width
&& covered[column + colspan] <= r
{
colspan += 1;
}
let mut rowspan = span_of(cell.rowspan).min(rows - r);
if r < header {
rowspan = rowspan.min(header - r);
}
let at = placed.len();
for c in column..column + colspan {
covered[c] = r + rowspan;
owner[c] = at;
}
placed.push(PlacedCell {
row: r,
column,
rowspan,
colspan,
align: cell.align.unwrap_or(aligns[column]),
parts: vec![&cell.content],
source: Some((r, index)),
});
next = column + colspan;
last = Some(at);
}
for column in 0..width {
if covered[column] <= r {
covered[column] = r + 1;
owner[column] = placed.len();
placed.push(PlacedCell {
row: r,
column,
rowspan: 1,
colspan: 1,
align: aligns[column],
parts: Vec::new(),
source: None,
});
}
}
}
placed.sort_by_key(|c| (c.row, c.column));
TableLayout {
columns: width,
rows,
header_rows: header,
aligns,
cells: placed,
}
}
pub fn normalized(&self) -> Table {
let layout = self.layout();
let mut columns = self.columns.clone();
columns.resize_with(layout.columns, TableColumn::default);
let mut rows: Vec<Vec<TableCell>> = vec![Vec::new(); layout.rows];
for cell in layout.cells {
let mut stored = cell
.source
.and_then(|(r, i)| self.rows.get(r)?.get(i).cloned())
.unwrap_or_else(|| TableCell::new(Vec::new()));
stored.content = cell.content();
stored.colspan = u32::try_from(cell.colspan).unwrap_or(u32::MAX);
stored.rowspan = u32::try_from(cell.rowspan).unwrap_or(u32::MAX);
if let Some(row) = rows.get_mut(cell.row) {
row.push(stored);
}
}
Table {
columns,
header_rows: u32::try_from(layout.header_rows).unwrap_or(u32::MAX),
rows,
caption: self.caption.clone(),
extra: self.extra.clone(),
}
}
}
fn span_of(n: u32) -> usize {
(n as usize).max(1)
}
fn has_note(inlines: &[Inline]) -> bool {
inlines.iter().any(|i| match i {
Inline::Footnote { .. } => true,
Inline::Emphasis { content }
| Inline::Strong { content }
| Inline::SmallCaps { content }
| Inline::Superscript { content }
| Inline::Subscript { content }
| Inline::Link { content, .. } => has_note(content),
Inline::Text { .. } | Inline::LineBreak => false,
})
}
#[cfg(test)]
mod tests;