Skip to main content

unparser_shared/
grid.rs

1//! Where a table's cells sit on its grid.
2//!
3//! Every parser in the family records a merged cell once, on the cell that owns it: the
4//! positions it covers — the tail of a horizontal span, and the columns a vertical span occupies
5//! in the rows below — have no cell of their own. So a cell's index in its row is not its grid
6//! column once a vertical span from a row above sits to its left, and a row's cell count is not
7//! the table's width once a cell spans columns. [`place`] walks the spans to recover both — the
8//! same walk whatever the document format was, done here once.
9//!
10//! `std`-only, like the rest of the crate root.
11
12/// The most columns [`place`] lets one cell own: a larger span is read as this many.
13///
14/// A span is a number the document declares — two bytes in one format, a decimal attribute in
15/// another — and the grid is as wide as the furthest any cell reaches, so one declared span
16/// would otherwise size every row of the laid-out table. The value is the one HTML's table
17/// processing model clamps `colspan` to (WHATWG HTML, «algorithm for processing rows»); no
18/// format in the family writes a wider merge except a spreadsheet's across its whole width.
19pub const MAX_COL_SPAN: u32 = 1000;
20
21/// The most rows [`place`] lets one cell own, the value HTML clamps `rowspan` to. A row span
22/// costs no allocation here — it only counts down rows that exist — but is read the same way
23/// HTML reads it, so a renderer that lays rows out itself can share the bound.
24pub const MAX_ROW_SPAN: u32 = 65_534;
25
26/// How many grid rows and columns a cell owns, its own position included. A span of 0 is read
27/// as 1; one past [`MAX_ROW_SPAN`] or [`MAX_COL_SPAN`] as that bound.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub struct Span {
30    /// Rows the cell owns.
31    pub rows: u32,
32    /// Columns the cell owns.
33    pub cols: u32,
34}
35
36impl Span {
37    /// A cell owning `rows` × `cols` grid positions.
38    pub fn new(rows: u32, cols: u32) -> Self {
39        Self { rows, cols }
40    }
41
42    /// A cell owning only its own position.
43    pub fn single() -> Self {
44        Self::new(1, 1)
45    }
46}
47
48/// Where each cell of a table sits: see [`place`].
49#[derive(Debug, Clone, PartialEq, Eq, Default)]
50pub struct Placement {
51    /// The grid column each cell starts in, one `Vec` per row, parallel to the row's cells.
52    pub columns: Vec<Vec<usize>>,
53    /// How many grid columns the table occupies: the furthest any cell reaches.
54    pub width: usize,
55}
56
57/// The grid column each cell starts in, and the table's width, for rows given as their cells'
58/// spans in reading order.
59///
60/// A cell starts at the first column, from where the row has got to, that no cell merged down
61/// from a row above still covers. The width is the furthest any cell reaches — which can be
62/// further than a row's own spans add up to, since a vertical span from above pushes the row's
63/// cells rightward.
64///
65/// ```
66/// use unparser_shared::grid::{place, Span};
67///
68/// // ┌────────┬───────────┐
69/// // │ Region │   Sales   │
70/// // │        ├─────┬─────┤
71/// // │        │2024 │2025 │
72/// // └────────┴─────┴─────┘
73/// let placement = place([
74///     vec![Span::new(2, 1), Span::new(1, 2)],
75///     vec![Span::single(), Span::single()],
76/// ]);
77/// assert_eq!(placement.columns, vec![vec![0, 1], vec![1, 2]]);
78/// assert_eq!(placement.width, 3);
79/// ```
80pub fn place<R, C>(rows: R) -> Placement
81where
82    R: IntoIterator<Item = C>,
83    C: IntoIterator<Item = Span>,
84{
85    // For each column, how many further rows a cell merged down from above still covers.
86    let mut covered: Vec<usize> = Vec::new();
87    let mut placement = Placement::default();
88    for row in rows {
89        let mut columns = Vec::new();
90        let mut col = 0;
91        for span in row {
92            // Pass the columns a cell from above still covers; this row uses them up once.
93            while covered.get(col).is_some_and(|&n| n > 0) {
94                covered[col] -= 1;
95                col += 1;
96            }
97            let (rows, cols) = (
98                span.rows.clamp(1, MAX_ROW_SPAN) as usize,
99                span.cols.clamp(1, MAX_COL_SPAN) as usize,
100            );
101            columns.push(col);
102            if covered.len() < col + cols {
103                covered.resize(col + cols, 0);
104            }
105            for c in &mut covered[col..col + cols] {
106                *c = rows - 1;
107            }
108            col += cols;
109            placement.width = placement.width.max(col);
110        }
111        // Columns past the row's last cell that a cell above still covers belong to this row.
112        for c in covered.iter_mut().skip(col) {
113            *c = c.saturating_sub(1);
114        }
115        placement.columns.push(columns);
116    }
117    placement
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn a_declared_span_past_the_bound_is_read_as_the_bound() {
126        let placement = place(spans(&[&[(u32::MAX, u32::MAX), (1, 1)], &[(1, 1)]]));
127        assert_eq!(
128            placement.columns,
129            vec![vec![0, MAX_COL_SPAN as usize], vec![MAX_COL_SPAN as usize]]
130        );
131        assert_eq!(placement.width, MAX_COL_SPAN as usize + 1);
132    }
133
134    fn spans(rows: &[&[(u32, u32)]]) -> Vec<Vec<Span>> {
135        rows.iter()
136            .map(|r| {
137                r.iter()
138                    .map(|&(rows, cols)| Span::new(rows, cols))
139                    .collect()
140            })
141            .collect()
142    }
143
144    #[test]
145    fn plain_cells_sit_at_their_index() {
146        let p = place(spans(&[&[(1, 1), (1, 1)], &[(1, 1), (1, 1)]]));
147        assert_eq!(p.columns, vec![vec![0, 1], vec![0, 1]]);
148        assert_eq!(p.width, 2);
149    }
150
151    /// A first row that merges across: its cell count is not the table's width.
152    #[test]
153    fn a_horizontal_span_in_the_first_row_widens_the_table() {
154        let p = place(spans(&[&[(2, 1), (1, 2)], &[(1, 1), (1, 1)]]));
155        assert_eq!(p.width, 3);
156    }
157
158    #[test]
159    fn a_vertical_span_pushes_the_rows_below_rightward() {
160        let p = place(spans(&[
161            &[(3, 1), (1, 1)],
162            &[(1, 1)],
163            &[(1, 1)],
164            &[(1, 1), (1, 1)],
165        ]));
166        assert_eq!(p.columns, vec![vec![0, 1], vec![1], vec![1], vec![0, 1]]);
167    }
168
169    #[test]
170    fn a_cell_merged_down_at_the_row_end_is_used_up_by_the_rows_it_covers() {
171        let p = place(spans(&[&[(1, 1), (2, 1)], &[(1, 1)], &[(1, 1), (1, 1)]]));
172        assert_eq!(p.columns, vec![vec![0, 1], vec![0], vec![0, 1]]);
173        assert_eq!(p.width, 2);
174    }
175
176    #[test]
177    fn a_block_merge_covers_rows_and_columns() {
178        let p = place(spans(&[
179            &[(2, 2), (1, 1)],
180            &[(1, 1)],
181            &[(1, 1), (1, 1), (1, 1)],
182        ]));
183        assert_eq!(p.columns, vec![vec![0, 2], vec![2], vec![0, 1, 2]]);
184        assert_eq!(p.width, 3);
185    }
186
187    #[test]
188    fn a_zero_span_is_read_as_one() {
189        let p = place(spans(&[&[(0, 0), (0, 0)]]));
190        assert_eq!(p.columns, vec![vec![0, 1]]);
191        assert_eq!(p.width, 2);
192    }
193
194    #[test]
195    fn an_empty_table_has_no_columns() {
196        assert_eq!(place(Vec::<Vec<Span>>::new()), Placement::default());
197    }
198}