Skip to main content

setzer_model/
table.rs

1//! The table block: rows and columns, header rows, per-column
2//! alignment, merged cells and a caption. A cell holds inlines.
3//!
4//! A stored table can be damaged (a hand-edited file, a bug), so nothing
5//! that draws a table reads `rows` by position: [`Table::layout`] turns any
6//! stored table into the one grid every renderer agrees on, and
7//! [`Table::normalized`] writes that grid back as a stored table. Reading a
8//! file never normalizes it.
9
10use crate::book::{Extra, Inline};
11use serde::{Deserialize, Serialize};
12
13/// The widest table [`Table::layout`] makes when `columns` is empty and the
14/// width has to be taken from the rows.
15pub const MAX_AUTO_COLUMNS: usize = 64;
16
17/// A table: `{ "type": "table", "columns": …, "rows": … }` in `book.json`
18/// (the block's JSON is flat; `Block::Table(Table)`).
19#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
20pub struct Table {
21    /// One entry per column: how its text is aligned. Its length is the
22    /// table's width.
23    #[serde(default)]
24    pub columns: Vec<TableColumn>,
25    /// The first `header_rows` rows are headers (0: none). They repeat on
26    /// every page in print and are `<thead>` in the EPUB.
27    #[serde(default, skip_serializing_if = "is_zero")]
28    pub header_rows: u32,
29    /// Row by row, only the cells that START in the row: a cell that a
30    /// rowspan above covers is not listed again.
31    #[serde(default)]
32    pub rows: Vec<Vec<TableCell>>,
33    /// Shown above the table.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub caption: Option<Vec<Inline>>,
36    /// Fields written by newer builds, kept on round-trip.
37    #[serde(flatten)]
38    pub extra: Extra,
39}
40
41/// One column of a [`Table`].
42#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
43pub struct TableColumn {
44    #[serde(default)]
45    pub align: ColumnAlign,
46    /// Fields written by newer builds, kept on round-trip.
47    #[serde(flatten)]
48    pub extra: Extra,
49}
50
51/// One cell of a [`Table`]: inlines only (a cell of two paragraphs is two
52/// lines of one cell, joined by `Inline::LineBreak`).
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub struct TableCell {
55    /// May be empty.
56    #[serde(default)]
57    pub content: Vec<Inline>,
58    /// How many columns the cell covers (1, and then not written).
59    #[serde(default = "one", skip_serializing_if = "is_one")]
60    pub colspan: u32,
61    /// How many rows the cell covers (1, and then not written).
62    #[serde(default = "one", skip_serializing_if = "is_one")]
63    pub rowspan: u32,
64    /// Overrides the column's alignment for this cell: a heading centred over
65    /// three right-aligned columns.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub align: Option<ColumnAlign>,
68    /// Fields written by newer builds, kept on round-trip.
69    #[serde(flatten)]
70    pub extra: Extra,
71}
72
73/// How the text of a column (or one cell) is aligned.
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
75#[serde(rename_all = "kebab-case")]
76pub enum ColumnAlign {
77    #[default]
78    Left,
79    Center,
80    Right,
81}
82
83fn is_zero(n: &u32) -> bool {
84    *n == 0
85}
86
87fn one() -> u32 {
88    1
89}
90
91fn is_one(n: &u32) -> bool {
92    *n == 1
93}
94
95impl TableColumn {
96    pub fn new(align: ColumnAlign) -> Self {
97        Self {
98            align,
99            extra: Extra::new(),
100        }
101    }
102}
103
104impl TableCell {
105    pub fn new(content: Vec<Inline>) -> Self {
106        Self {
107            content,
108            colspan: 1,
109            rowspan: 1,
110            align: None,
111            extra: Extra::new(),
112        }
113    }
114
115    /// A cell of plain text.
116    pub fn text(s: impl Into<String>) -> Self {
117        Self::new(vec![Inline::text(s)])
118    }
119
120    /// The cell covers `colspan` columns and `rowspan` rows.
121    pub fn with_span(mut self, colspan: u32, rowspan: u32) -> Self {
122        self.colspan = colspan;
123        self.rowspan = rowspan;
124        self
125    }
126
127    pub fn with_align(mut self, align: ColumnAlign) -> Self {
128        self.align = Some(align);
129        self
130    }
131}
132
133/// A cell of [`Table::layout`]: where it stands in the grid, what it covers.
134/// It borrows its text from the table, so a footnote in it can be kept for as
135/// long as the table lives (the EPUB writer collects notes by reference).
136#[derive(Debug, Clone, PartialEq)]
137pub struct PlacedCell<'t> {
138    pub row: usize,
139    pub column: usize,
140    /// At least 1; never past the grid, never across the header boundary.
141    pub rowspan: usize,
142    pub colspan: usize,
143    /// The cell's own alignment, or else its first column's.
144    pub align: ColumnAlign,
145    /// The text, in parts: the stored cell's own content first (maybe empty),
146    /// then the content of each stored cell of its row that had no room (rule
147    /// 3), non-empty ones only. The parts are one cell, one under the other,
148    /// a line break between them: [`PlacedCell::content`] joins them. Empty
149    /// for a cell the layout made up.
150    pub parts: Vec<&'t [Inline]>,
151    /// `(row, index in the row's list)` of the stored cell; `None` for an
152    /// empty cell the layout made up for a position nothing covered.
153    pub source: Option<(usize, usize)>,
154}
155
156/// The table the model means: a complete grid. Every position
157/// is covered exactly once, every row has a cell of its own, no span crosses
158/// the table's edge or the header boundary. Cells are in reading order (by
159/// row, then column).
160#[derive(Debug, Clone, PartialEq)]
161pub struct TableLayout<'t> {
162    /// Width of the grid.
163    pub columns: usize,
164    /// Height of the grid (the stored rows).
165    pub rows: usize,
166    /// The leading rows that are headers, at most `rows`.
167    pub header_rows: usize,
168    /// The alignment of each column, `columns` of them.
169    pub aligns: Vec<ColumnAlign>,
170    pub cells: Vec<PlacedCell<'t>>,
171}
172
173impl<'t> PlacedCell<'t> {
174    /// The cell's text as one run of inlines, the parts joined by line
175    /// breaks (cloned; use `parts` to keep borrowing). A part that shows
176    /// nothing (the stored cell's own, when it was empty) adds no line break.
177    pub fn content(&self) -> Vec<Inline> {
178        let mut out = Vec::new();
179        for part in self.shown_parts() {
180            if !out.is_empty() {
181                out.push(Inline::LineBreak);
182            }
183            out.extend(part.iter().cloned());
184        }
185        out
186    }
187
188    /// The parts that show something (text or a footnote): what a writer
189    /// joins with line breaks, so that none starts or ends the cell.
190    pub fn shown_parts(&self) -> impl Iterator<Item = &'t [Inline]> + '_ {
191        self.parts
192            .iter()
193            .copied()
194            .filter(|p| !Inline::plain_text(p).trim().is_empty() || has_note(p))
195    }
196
197    /// Whether the cell shows nothing: no text and no footnote.
198    pub fn is_blank(&self) -> bool {
199        self.parts
200            .iter()
201            .all(|p| Inline::plain_text(p).trim().is_empty() && !has_note(p))
202    }
203}
204
205impl Table {
206    /// A table, normalized (see [`Table::normalized`]): what the importers
207    /// build. `columns` may be empty, then the width is the widest row's.
208    pub fn new(columns: Vec<TableColumn>, header_rows: u32, rows: Vec<Vec<TableCell>>) -> Self {
209        Table {
210            columns,
211            header_rows,
212            rows,
213            caption: None,
214            extra: Extra::new(),
215        }
216        .normalized()
217    }
218
219    /// The same table with a caption shown above it.
220    pub fn with_caption(mut self, caption: Vec<Inline>) -> Self {
221        self.caption = Some(caption);
222        self
223    }
224
225    /// The width of the grid: `columns.len()`, or with no columns the widest
226    /// row (the sum of its cells' spans), at most [`MAX_AUTO_COLUMNS`] and at
227    /// least one when there is a row.
228    fn width(&self) -> usize {
229        if !self.columns.is_empty() {
230            return self.columns.len();
231        }
232        let widest = self
233            .rows
234            .iter()
235            .map(|row| {
236                row.iter()
237                    .fold(0usize, |sum, c| sum.saturating_add(span_of(c.colspan)))
238            })
239            .max()
240            .unwrap_or(0);
241        if self.rows.is_empty() {
242            0
243        } else {
244            widest.clamp(1, MAX_AUTO_COLUMNS)
245        }
246    }
247
248    /// The grid this table means, by the seven rules below. Total:
249    /// no stored table panics or loses a cell's text.
250    ///
251    /// 1. The width is `columns.len()` (or the widest row, see [`Self::width`]).
252    /// 2. A span of 0 is 1; a `colspan` is cut at the width and at a position
253    ///    a rowspan above covers.
254    /// 3. Cells are placed row by row, each at the first position at or after
255    ///    the end of the previous one that no rowspan above covers. A cell
256    ///    that does not fit has its text appended, after a line break, to the
257    ///    last cell placed in its row.
258    /// 4. A `rowspan` is cut at the last row and at the header boundary (a
259    ///    cell that starts in a header row covers header rows only). A
260    ///    rectangle cannot meet a position already taken: placement goes row
261    ///    by row, so positions below a cell are free until a later row takes
262    ///    them, and that row avoids the covered ones.
263    /// 5. A position nothing covers and no cell was placed on becomes an empty
264    ///    cell.
265    /// 6. A row whose every position is covered by rowspans from above cuts
266    ///    those spans to end above it, and is then placed like any row (its
267    ///    listed cells take the freed positions, the rest become empty). The
268    ///    ADR drops the text of such a row's listed cells; this keeps it,
269    ///    since the rule only exists so that no row is without a cell.
270    /// 7. `header_rows` is cut to the number of rows.
271    pub fn layout(&self) -> TableLayout<'_> {
272        let rows = self.rows.len();
273        let width = self.width();
274        let header = (self.header_rows as usize).min(rows);
275        let aligns: Vec<ColumnAlign> = (0..width)
276            .map(|c| self.columns.get(c).map_or(ColumnAlign::Left, |c| c.align))
277            .collect();
278        let mut placed: Vec<PlacedCell> = Vec::new();
279        // `covered[c]`: the first row below the rowspan that covers column
280        // `c`; the column is covered in row `r` while `r < covered[c]`.
281        let mut covered = vec![0usize; width];
282        // The placed cell that covers column `c` as far down as `covered[c]`.
283        let mut owner: Vec<usize> = vec![0; width];
284
285        for (r, listed) in self.rows.iter().enumerate() {
286            // Rule 6: no position left for the row's own cells.
287            if covered.iter().all(|&below| below > r) {
288                let mut cut: Vec<usize> = owner.clone();
289                cut.sort_unstable();
290                cut.dedup();
291                for i in cut {
292                    let cell = &mut placed[i];
293                    cell.rowspan = r - cell.row;
294                    covered[cell.column..cell.column + cell.colspan].fill(r);
295                }
296            }
297            let mut next = 0;
298            let mut last: Option<usize> = None;
299            for (index, cell) in listed.iter().enumerate() {
300                let mut column = next;
301                while column < width && covered[column] > r {
302                    column += 1;
303                }
304                if column >= width {
305                    // Rule 3: no room. A position was free for the row's first
306                    // cell, so there is a cell placed in the row to take it.
307                    if let Some(to) = last
308                        && !cell.content.is_empty()
309                    {
310                        placed[to].parts.push(&cell.content);
311                    }
312                    continue;
313                }
314                let mut colspan = 1;
315                while colspan < span_of(cell.colspan)
316                    && column + colspan < width
317                    && covered[column + colspan] <= r
318                {
319                    colspan += 1;
320                }
321                let mut rowspan = span_of(cell.rowspan).min(rows - r);
322                if r < header {
323                    rowspan = rowspan.min(header - r);
324                }
325                let at = placed.len();
326                for c in column..column + colspan {
327                    covered[c] = r + rowspan;
328                    owner[c] = at;
329                }
330                placed.push(PlacedCell {
331                    row: r,
332                    column,
333                    rowspan,
334                    colspan,
335                    align: cell.align.unwrap_or(aligns[column]),
336                    parts: vec![&cell.content],
337                    source: Some((r, index)),
338                });
339                next = column + colspan;
340                last = Some(at);
341            }
342            // Rule 5: positions nothing covers.
343            for column in 0..width {
344                if covered[column] <= r {
345                    covered[column] = r + 1;
346                    owner[column] = placed.len();
347                    placed.push(PlacedCell {
348                        row: r,
349                        column,
350                        rowspan: 1,
351                        colspan: 1,
352                        align: aligns[column],
353                        parts: Vec::new(),
354                        source: None,
355                    });
356                }
357            }
358        }
359        placed.sort_by_key(|c| (c.row, c.column));
360        TableLayout {
361            columns: width,
362            rows,
363            header_rows: header,
364            aligns,
365            cells: placed,
366        }
367    }
368
369    /// The layout written back as a stored table, in the form importers and
370    /// [`Table::new`] make: one column entry per position, only the cells that
371    /// start in each row, spans cut, gaps filled with empty cells,
372    /// `header_rows` cut. Its layout is this table's, cell for cell (the
373    /// `source` of a cell made up now points at its stored place).
374    pub fn normalized(&self) -> Table {
375        let layout = self.layout();
376        let mut columns = self.columns.clone();
377        columns.resize_with(layout.columns, TableColumn::default);
378        let mut rows: Vec<Vec<TableCell>> = vec![Vec::new(); layout.rows];
379        for cell in layout.cells {
380            let mut stored = cell
381                .source
382                .and_then(|(r, i)| self.rows.get(r)?.get(i).cloned())
383                .unwrap_or_else(|| TableCell::new(Vec::new()));
384            stored.content = cell.content();
385            stored.colspan = u32::try_from(cell.colspan).unwrap_or(u32::MAX);
386            stored.rowspan = u32::try_from(cell.rowspan).unwrap_or(u32::MAX);
387            if let Some(row) = rows.get_mut(cell.row) {
388                row.push(stored);
389            }
390        }
391        Table {
392            columns,
393            header_rows: u32::try_from(layout.header_rows).unwrap_or(u32::MAX),
394            rows,
395            caption: self.caption.clone(),
396            extra: self.extra.clone(),
397        }
398    }
399}
400
401/// A span as a count: 0 (a damaged file) is 1.
402fn span_of(n: u32) -> usize {
403    (n as usize).max(1)
404}
405
406/// Whether `inlines` hold a footnote, which `plain_text` leaves out.
407fn has_note(inlines: &[Inline]) -> bool {
408    inlines.iter().any(|i| match i {
409        Inline::Footnote { .. } => true,
410        Inline::Emphasis { content }
411        | Inline::Strong { content }
412        | Inline::SmallCaps { content }
413        | Inline::Superscript { content }
414        | Inline::Subscript { content }
415        | Inline::Link { content, .. } => has_note(content),
416        Inline::Text { .. } | Inline::LineBreak => false,
417    })
418}
419
420#[cfg(test)]
421mod tests;