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}