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;