//! Tables: a column grid, rows and cells (cells hold block lists). Merges follow OOXML: a cell //! spans `span` grid columns; vertical merges are `Restart` + `Continue` cells. use serde::{Deserialize, Serialize}; use crate::control::ControlWrap; use crate::props::{CellProps, RowProps, TableProps, VMerge}; use crate::{Blocks, Paragraph, para_block}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)] pub struct Cell { #[serde(default)] pub props: CellProps, pub blocks: Blocks, /// Content controls around the cell (see [`crate::control`]). #[serde(default, skip_serializing_if = "ControlWrap::is_empty")] pub controls: ControlWrap, } impl Cell { pub fn empty() -> Cell { Cell { props: CellProps { span: 1, ..Default::default() }, blocks: vec![para_block(Paragraph::new())], controls: ControlWrap::default() } } pub fn with_text(s: &str) -> Cell { Cell { props: CellProps { span: 1, ..Default::default() }, blocks: vec![para_block(Paragraph::with_text(s, Default::default()))], controls: ControlWrap::default(), } } pub fn span(&self) -> usize { self.props.span.clamp(1, 63) as usize } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)] pub struct Row { #[serde(default)] pub props: RowProps, pub cells: Vec, /// Content controls around the row (repeating sections; see [`crate::control`]). #[serde(default, skip_serializing_if = "ControlWrap::is_empty")] pub controls: ControlWrap, } impl Row { /// Grid column where the row's first cell starts: the columns `w:gridBefore` skips. pub fn first_grid(&self) -> usize { (self.props.grid_before as usize).min(MAX_COLS) } /// Grid columns `w:gridAfter` leaves empty after the row's last cell, bounded as the DOCX /// reader bounds them (a native document can say anything). pub fn grid_after(&self) -> usize { (self.props.grid_after as usize).min(MAX_COLS) } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)] pub struct Table { #[serde(default)] pub props: TableProps, /// Grid column widths, points. pub grid: Vec, pub rows: Vec, /// Content controls whose content starts or ends with this table (see [`crate::control`]). #[serde(default, skip_serializing_if = "ControlWrap::is_empty")] pub controls: ControlWrap, } /// Hard limits that keep hostile input from exhausting memory. pub const MAX_ROWS: usize = 32_767; pub const MAX_COLS: usize = 63; /// Most tables nested one inside another: the depth the DOCX reader and writer keep. Pasting /// and inserting stop there too, so no edit builds a document deep enough to overflow the stack /// when it is saved, copied or dropped (#1596). pub const MAX_TABLE_DEPTH: usize = 24; impl Table { /// Grid columns `lo..hi` that [`Table::slice`] copies for `rows`/`cols` (widened over merged cells), /// and whether any cell touches them (Cut clears the same cells it copies). pub fn slice_bounds(&self, rows: (usize, usize), cols: (usize, usize)) -> (usize, usize, bool) { let ((r0, r1), (g0, g1)) = (rows, cols); let kept = || self.rows.iter().take(r1.saturating_add(1)).skip(r0); // Grid columns `lo..hi` the cells touching them cover: a cell merged across that is partly // in them widens them, for every row, so the copy isn't ragged. Each round only widens // (at most once per grid column), so a bound of the grid's width plus one round is enough. let (mut lo, mut hi) = (g0, g1.saturating_add(1)); let mut any = false; for _ in 0..=MAX_COLS.saturating_add(1) { let (mut nlo, mut nhi, mut found) = (lo, hi, false); for row in kept() { let mut g = 0usize; for cell in &row.cells { let end = g.saturating_add(cell.span()); if g < hi && end > lo { (nlo, nhi, found) = (nlo.min(g), nhi.max(end), true); } g = end; } } any = found; if (nlo, nhi) == (lo, hi) { break; } (lo, hi) = (nlo, nhi); } (lo, hi, any) } /// A copy of rows `rows` of this table holding, of each row, the cells that reach into grid /// columns `cols` (both inclusive): what Copy takes from selected rows or cells. Content /// controls around rows and the table aren't copied (they'd lose their other end); a cell /// that continues a vertical merge from a row left out starts the merge instead. pub fn slice(&self, rows: (usize, usize), cols: (usize, usize)) -> Table { let (r0, r1) = rows; let kept = || self.rows.iter().take(r1.saturating_add(1)).skip(r0); let (lo, hi, any) = self.slice_bounds(rows, cols); let mut out_rows = Vec::new(); for row in kept() { let mut g = 0usize; let mut cells = Vec::new(); for cell in &row.cells { let end = g.saturating_add(cell.span()); if any && g < hi && end > lo { cells.push(cell.clone()); } g = end; } if !cells.is_empty() { out_rows.push(Row { props: row.props.clone(), cells, controls: ControlWrap::default() }); } } if let Some(first) = out_rows.first_mut() { for c in &mut first.cells { if c.props.vmerge == VMerge::Continue { c.props.vmerge = VMerge::Restart; } } } let whole = lo == 0 && hi >= self.grid.len(); let grid = if whole { self.grid.clone() } else { self.grid.get(lo..hi.min(self.grid.len())).map(<[f32]>::to_vec).unwrap_or_default() }; let mut props = self.props.clone(); if !whole { // Part of the columns: as wide as they are, not the whole table's width. if props.width.is_some() || props.width_pct.is_some() { props.width = Some(grid.iter().sum()); } props.width_pct = None; } Table { props, grid, rows: out_rows, controls: ControlWrap::default() } } /// A `rows × cols` table spanning `width` points, "Table Grid" style. pub fn new(rows: usize, cols: usize, width: f32) -> Table { let rows = rows.clamp(1, MAX_ROWS); let cols = cols.clamp(1, MAX_COLS); let w = (width / cols as f32).max(6.0); Table { props: TableProps { style: Some("TableGrid".into()), ..Default::default() }, grid: vec![w; cols], rows: (0..rows) .map(|_| Row { props: RowProps::default(), cells: (0..cols) .map(|_| { let mut c = Cell::empty(); c.props.width = Some(w); c }) .collect(), controls: ControlWrap::default(), }) .collect(), controls: ControlWrap::default(), } } pub fn cols(&self) -> usize { let row_cols = |r: &Row| r.first_grid().saturating_add(r.cells.iter().map(Cell::span).fold(0, usize::saturating_add)).saturating_add(r.grid_after()); self.grid.len().max(self.rows.iter().map(row_cols).max().unwrap_or(0)) } /// Grid column where cell `c` of row `r` starts. pub fn grid_col(&self, r: usize, c: usize) -> usize { self.rows.get(r).map(|row| row.first_grid() + row.cells.iter().take(c).map(Cell::span).sum::()).unwrap_or(0) } /// Cell index in row `r` that covers grid column `g`. pub fn cell_at_grid(&self, r: usize, g: usize) -> Option { let row = self.rows.get(r)?; let mut x = row.first_grid(); if g < x { return None; } for (i, c) in row.cells.iter().enumerate() { if g < x + c.span() { return Some(i); } x += c.span(); } None } /// Insert a row copied (formatting, empty text) from row `like` at index `at`. pub fn insert_row(&mut self, at: usize, like: usize) { let Some(src) = self.rows.get(like.min(self.rows.len().saturating_sub(1))) else { return }; if self.rows.len() >= MAX_ROWS { return; } let mut row = Row { props: src.props.clone(), cells: Vec::with_capacity(src.cells.len()), controls: ControlWrap::default() }; row.props.header = false; for c in &src.cells { let mut nc = Cell::empty(); nc.props = c.props.clone(); if nc.props.vmerge == VMerge::Restart { nc.props.vmerge = VMerge::None; } row.cells.push(nc); } self.rows.insert(at.min(self.rows.len()), row); } pub fn delete_row(&mut self, r: usize) { if r >= self.rows.len() { return; } // A vertical merge that starts in this row now starts in the row below. let starts: Vec = (0..self.rows.get(r).map_or(0, |row| row.cells.len())) .filter(|&c| self.rows.get(r).and_then(|row| row.cells.get(c)).is_some_and(|x| x.props.vmerge == VMerge::Restart)) .map(|c| self.grid_col(r, c)) .collect(); for g in starts { let continues = |t: &Self, row: usize| { t.cell_at_grid(row, g).and_then(|i| t.rows.get(row)?.cells.get(i)).is_some_and(|x| x.props.vmerge == VMerge::Continue) }; if !continues(self, r + 1) { continue; } let more = continues(self, r + 2); if let Some(c) = self.cell_at_grid(r + 1, g).and_then(|i| self.rows.get_mut(r + 1)?.cells.get_mut(i)) { c.props.vmerge = if more { VMerge::Restart } else { VMerge::None }; } } self.rows.remove(r); } /// Split the table before row `at`: the rows from `at` on move to the returned table, which /// has this table's properties and grid. A vertical merge that the split cuts ends in this /// table's last row and starts again in the new table's first row. A row content control /// around rows on both sides closes after this table's last row and opens again (without its /// `w:id`) before the new table's first row. Content controls that close after this table /// close after the new one. pub fn split_off(&mut self, at: usize) -> Table { let at = at.min(self.rows.len()); let mut open = Vec::new(); for row in self.rows.iter().take(at) { open.extend(row.controls.open.iter().cloned()); let close = usize::try_from(row.controls.close).unwrap_or(usize::MAX).min(open.len()); open.truncate(open.len() - close); } let mut tail = Table { props: self.props.clone(), grid: self.grid.clone(), rows: self.rows.split_off(at), controls: ControlWrap { open: Vec::new(), close: std::mem::take(&mut self.controls.close) }, }; if let Some(last) = self.rows.last_mut() { last.controls.close = last.controls.close.saturating_add(u32::try_from(open.len()).unwrap_or(u32::MAX)); for c in &mut last.cells { if c.props.vmerge == VMerge::Restart { c.props.vmerge = VMerge::None; } } } for c in &mut open { c.id = None; } if let Some(first) = tail.rows.first_mut() { let own = std::mem::replace(&mut first.controls.open, open); first.controls.open.extend(own); } let starts: Vec<(usize, usize)> = (0..tail.rows.first().map_or(0, |row| row.cells.len())) .filter(|&c| tail.rows.first().and_then(|row| row.cells.get(c)).is_some_and(|x| x.props.vmerge == VMerge::Continue)) .map(|c| (c, tail.grid_col(0, c))) .collect(); for (c, g) in starts { let more = tail.cell_at_grid(1, g).and_then(|i| tail.rows.get(1)?.cells.get(i)).is_some_and(|x| x.props.vmerge == VMerge::Continue); if let Some(x) = tail.rows.first_mut().and_then(|r| r.cells.get_mut(c)) { x.props.vmerge = if more { VMerge::Restart } else { VMerge::None }; } } tail } /// Insert a grid column at grid index `g` (cells spanning it widen instead). pub fn insert_col(&mut self, g: usize) { if self.grid.len() >= MAX_COLS { return; } let g = g.min(self.grid.len()); let w = self.grid.get(g.min(self.grid.len().saturating_sub(1))).copied().unwrap_or(72.0); self.grid.insert(g, w); for r in 0..self.rows.len() { let mut x = 0; let mut idx = None; if let Some(row) = self.rows.get(r) { for (i, c) in row.cells.iter().enumerate() { if g <= x { idx = Some((i, false)); break; } if g < x + c.span() { idx = Some((i, true)); break; } x += c.span(); } } if let Some(row) = self.rows.get_mut(r) { match idx { Some((i, true)) => { if let Some(c) = row.cells.get_mut(i) { // From the clamped span: a `.json` file can carry any `u32` (#3637). c.props.span = (c.span() + 1).min(MAX_COLS) as u32; } } Some((i, false)) => { let mut c = Cell::empty(); c.props.width = Some(w); row.cells.insert(i, c); } None => { let mut c = Cell::empty(); c.props.width = Some(w); row.cells.push(c); } } } } } /// Delete grid column `g` (spanning cells shrink). pub fn delete_col(&mut self, g: usize) { if g >= self.grid.len() || self.grid.len() <= 1 { return; } self.grid.remove(g); for row in &mut self.rows { let mut x = 0; let mut rm = None; for (i, c) in row.cells.iter_mut().enumerate() { if g < x + c.span() { if c.span() > 1 { c.props.span = (c.span() - 1) as u32; } else { rm = Some(i); } break; } x += c.span(); } if let Some(i) = rm { row.cells.remove(i); } } self.rows.retain(|r| !r.cells.is_empty()); } /// Merge cells `c0..=c1` of row `r` horizontally (content is concatenated). pub fn merge_right(&mut self, r: usize, c0: usize, c1: usize) { let Some(row) = self.rows.get_mut(r) else { return }; if c1 <= c0 || c1 >= row.cells.len() { return; } let removed: Vec = row.cells.drain(c0 + 1..=c1).collect(); if let Some(first) = row.cells.get_mut(c0) { for c in removed { first.props.span = (first.span() + c.span()).min(MAX_COLS) as u32; let nonempty: Vec<_> = c.blocks.into_iter().filter(|b| b.as_para().is_none_or(|p| !p.is_empty())).collect(); first.blocks.extend(nonempty); } if first.blocks.len() > 1 && first.blocks.first().and_then(|b| b.as_para()).is_some_and(Paragraph::is_empty) { first.blocks.remove(0); } } } /// Merge a rectangle of cells (rows r0..=r1, grid columns g0..=g1). pub fn merge(&mut self, r0: usize, r1: usize, g0: usize, g1: usize) { for r in r0..=r1.min(self.rows.len().saturating_sub(1)) { let (Some(a), Some(b)) = (self.cell_at_grid(r, g0), self.cell_at_grid(r, g1)) else { continue }; self.merge_right(r, a, b); if let Some(c) = self.rows.get_mut(r).and_then(|row| row.cells.get_mut(a)) { c.props.vmerge = if r1 == r0 { VMerge::None } else if r == r0 { VMerge::Restart } else { VMerge::Continue }; } } // Move content of continued cells into the restart cell. if r1 > r0 && let Some(a) = self.cell_at_grid(r0, g0) { let mut moved = Vec::new(); for r in r0 + 1..=r1.min(self.rows.len().saturating_sub(1)) { if let Some(c) = self.cell_at_grid(r, g0).and_then(|ci| self.rows.get_mut(r).and_then(|row| row.cells.get_mut(ci))) { let blocks = std::mem::replace(&mut c.blocks, vec![para_block(Paragraph::new())]); moved.extend(blocks.into_iter().filter(|b| b.as_para().is_none_or(|p| !p.is_empty()))); } } if let Some(c) = self.rows.get_mut(r0).and_then(|row| row.cells.get_mut(a)) { c.blocks.extend(moved); } } } /// Split cell `c` of row `r` into `n` cells horizontally. Where the cell's grid lines don't /// fall on the equal parts, the grid is refined inside the cell only: the new borders divide it /// into equal parts, and the cells of other rows widen over the new grid columns, so their /// extent and the table's width stay as they were. Returns the parts' spans, left to right. pub fn split_cell(&mut self, r: usize, c: usize, n: usize) -> Vec { let n = n.clamp(1, MAX_COLS); let Some(span) = self.rows.get(r).and_then(|row| row.cells.get(c)).map(Cell::span) else { return Vec::new() }; if n <= 1 { return vec![span]; } let g = self.grid_col(r, c); let mut end = g.saturating_add(span); // A grid narrower than the row (a damaged file) gets the columns the cell needs. while self.grid.len() < end && self.grid.len() < MAX_COLS { self.grid.push(72.0); } if self.grid.len() < end { return vec![span]; } for w in self.grid.get_mut(g..end).into_iter().flatten() { if !(w.is_finite() && *w > 0.0) { *w = 72.0; } } const EPS: f32 = 0.01; let total: f32 = self.grid.get(g..end).map(|x| x.iter().sum()).unwrap_or(0.0); // Grid lines where the cell's parts start (after the first, at `g`). let mut lines = Vec::with_capacity(n); for k in 1..n { let x = total * k as f32 / n as f32; let mut acc = 0.0; let mut hit = None; for j in g..end { let w = self.grid.get(j).copied().unwrap_or(0.0); if j > g && (x - acc).abs() <= EPS { hit = Some((j, None)); break; } if x < acc + w - EPS { if x > acc + EPS { hit = Some((j, Some((x - acc, w - (x - acc))))); } break; } acc += w; } match hit { // An existing grid line is where the border goes. Some((j, None)) => lines.push(j), // A new grid line inside column `j`: every cell over `j` widens over both halves. Some((j, Some((left, right)))) if self.grid.len() < MAX_COLS => { if let Some(w) = self.grid.get_mut(j) { *w = left; } self.grid.insert(j + 1, right); for rr in 0..self.rows.len() { if let Some(ci) = self.cell_at_grid(rr, j) && let Some(cell) = self.rows.get_mut(rr).and_then(|row| row.cells.get_mut(ci)) { cell.props.span = cell.props.span.max(1).saturating_add(1); } } end += 1; lines.push(j + 1); } _ => {} } } lines.dedup(); // The cell now spans `g..end`: cut it at the new lines. let grid = self.grid.clone(); let width = |a: usize, b: usize| grid.get(a..b).map(|x| x.iter().sum::()); let Some(row) = self.rows.get_mut(r) else { return Vec::new() }; let mut a = g; let mut parts = Vec::with_capacity(lines.len() + 1); for &b in lines.iter().chain(std::iter::once(&end)) { if b > a { parts.push((a, b)); a = b; } } for (k, &(a, b)) in parts.iter().enumerate() { if k == 0 { if let Some(cell) = row.cells.get_mut(c) { cell.props.span = (b - a) as u32; cell.props.width = width(a, b); } } else { let mut nc = Cell::empty(); nc.props.span = (b - a) as u32; nc.props.width = width(a, b); row.cells.insert((c + k).min(row.cells.len()), nc); } } parts.iter().map(|&(a, b)| b - a).collect() } /// The rows cell `c` of row `r` covers: (first row, count). A vertically merged cell runs from /// its `Restart` cell down through the `Continue` cells that start at the same grid column. pub fn vertical_extent(&self, r: usize, c: usize) -> Option<(usize, usize)> { let g = self.grid_col(r, c); let at = |rr: usize| -> Option { let ci = self.cell_at_grid(rr, g)?; if self.grid_col(rr, ci) != g { return None; } Some(self.rows.get(rr)?.cells.get(ci)?.props.vmerge) }; let mut top = r; if self.rows.get(r)?.cells.get(c)?.props.vmerge == VMerge::Continue { while let Some(up) = top.checked_sub(1).and_then(at) { if up == VMerge::None { break; } top -= 1; if up == VMerge::Restart { break; } } } let mut k = 1; if at(top) != Some(VMerge::None) { while at(top + k) == Some(VMerge::Continue) { k += 1; } } Some((top, k)) } /// Split Cells: cut cell `c` of row `r` into `cols` × `rows` cells. A cell one row high gets /// `rows - 1` new rows under it, over which the row's other cells merge down; a vertically /// merged cell is regrouped instead, so `rows` must divide its height. The content stays in /// the first part. Returns false (with the table unchanged) when the split can't be made. pub fn split_cell_grid(&mut self, r: usize, c: usize, cols: usize, rows: usize) -> bool { let (cols, rows) = (cols.clamp(1, MAX_COLS), rows.clamp(1, MAX_ROWS)); let Some((top, k)) = self.vertical_extent(r, c) else { return false }; let g = self.grid_col(r, c); let Some(ci) = self.cell_at_grid(top, g) else { return false }; if (k > 1 && k % rows != 0) || (k == 1 && self.rows.len().saturating_add(rows - 1) > MAX_ROWS) { return false; } let height = if k > 1 { k } else { rows }; if k > 1 && rows > 1 { let q = k / rows; for i in 0..k { if let Some(cj) = self.cell_at_grid(top + i, g) && let Some(cell) = self.rows.get_mut(top + i).and_then(|row| row.cells.get_mut(cj)) { cell.props.vmerge = if q == 1 { VMerge::None } else if i % q == 0 { VMerge::Restart } else { VMerge::Continue }; } } } else if k == 1 && rows > 1 { let Some(src) = self.rows.get_mut(top) else { return false }; let mut like = Row { props: src.props.clone(), cells: Vec::with_capacity(src.cells.len()), controls: ControlWrap::default() }; like.props.header = false; for (i, cell) in src.cells.iter_mut().enumerate() { let mut nc = Cell::empty(); nc.props = cell.props.clone(); if i == ci { cell.props.vmerge = VMerge::None; nc.props.vmerge = VMerge::None; } else { // The row's other cells merge down over the new rows. if cell.props.vmerge == VMerge::None { cell.props.vmerge = VMerge::Restart; } nc.props.vmerge = VMerge::Continue; } like.cells.push(nc); } for _ in 1..rows { self.rows.insert(top + 1, like.clone()); } } if cols > 1 { let spans = self.split_cell(top, ci, cols); let total: usize = spans.iter().sum(); let vmerge = self.rows.get(top).and_then(|row| row.cells.get(ci)).map(|cell| cell.props.vmerge).unwrap_or_default(); if let Some(row) = self.rows.get_mut(top) { for cell in row.cells.iter_mut().skip(ci + 1).take(spans.len().saturating_sub(1)) { cell.props.vmerge = vmerge; } } // The cells under it, which the new grid lines widened, are cut the same way. let grid = self.grid.clone(); let width = |a: usize, b: usize| grid.get(a..b).map(|x| x.iter().sum::()); for rr in top + 1..top + height { let Some(cj) = self.cell_at_grid(rr, g).filter(|&cj| self.grid_col(rr, cj) == g) else { continue }; let Some(row) = self.rows.get_mut(rr) else { continue }; let Some(cell) = row.cells.get_mut(cj).filter(|cell| cell.span() == total) else { continue }; let mut a = g; let mut parts = Vec::with_capacity(spans.len()); for (n, &span) in spans.iter().enumerate() { let mut part = if n == 0 { cell.clone() } else { Cell::empty() }; part.props.vmerge = cell.props.vmerge; part.props.span = span as u32; part.props.width = width(a, a + span); a += span; parts.push(part); } row.cells.splice(cj..=cj, parts); } } true } pub fn total_width(&self) -> f32 { self.grid.iter().sum() } } #[cfg(test)] mod tests { use super::*; /// A 3×3 table, 100 pt columns: (0,0)–(0,1) merged across, (1,2)–(2,2) merged down. fn merged() -> Table { let mut t = Table::new(3, 3, 300.0); t.merge(0, 0, 0, 1); t.merge(1, 2, 2, 2); assert_eq!(t.rows[0].cells.iter().map(Cell::span).collect::>(), [2, 1]); assert_eq!((t.rows[1].cells[2].props.vmerge, t.rows[2].cells[2].props.vmerge), (VMerge::Restart, VMerge::Continue)); t } fn spans(t: &Table) -> Vec> { t.rows.iter().map(|r| r.cells.iter().map(Cell::span).collect()).collect() } /// #1744 review: a cell merged across that is partly selected takes its whole width, and the /// other rows widen with it, so the copy isn't ragged. #[test] fn slice_widens_to_a_merged_cell_in_every_row() { let t = merged().slice((0, 1), (1, 2)); assert_eq!(t.grid, [100.0, 100.0, 100.0]); assert_eq!(spans(&t), [vec![2, 1], vec![1, 1, 1]]); // Rows without the merged cell: just the selected columns. let t = merged().slice((1, 2), (1, 2)); assert_eq!(t.grid, [100.0, 100.0]); assert_eq!(spans(&t), [vec![1, 1], vec![1, 1]]); } /// #1744 review: a cell continuing a vertical merge from a row left out starts it instead. #[test] fn slice_starts_a_vertical_merge_whose_first_row_is_left_out() { let t = merged().slice((2, 2), (0, 2)); assert_eq!(t.rows.len(), 1); assert_eq!(t.rows[0].cells[2].props.vmerge, VMerge::Restart); // Both rows kept: the merge is kept as it is. let t = merged().slice((1, 2), (2, 2)); assert_eq!((t.rows[0].cells[0].props.vmerge, t.rows[1].cells[0].props.vmerge), (VMerge::Restart, VMerge::Continue)); } /// #1744 review: part of the columns is as wide as they are; content controls around rows /// and the table aren't copied. #[test] fn slice_width_and_controls() { let mut t = Table::new(3, 3, 300.0); (t.props.width, t.props.width_pct) = (Some(300.0), Some(50.0)); let wrap = ControlWrap { open: vec![crate::control::ContentControl::default()], close: 1 }; t.controls = wrap.clone(); t.rows[1].controls = wrap; let part = t.slice((0, 2), (2, 2)); assert_eq!((part.grid.len(), part.props.width, part.props.width_pct), (1, Some(100.0), None)); let whole = t.slice((0, 2), (0, 2)); assert_eq!((whole.props.width, whole.props.width_pct), (Some(300.0), Some(50.0)), "all columns keep the table's width"); let row = t.slice((1, 1), (0, 2)); assert!(row.controls.is_empty() && row.rows[0].controls.is_empty()); // A table sized only by a percentage keeps a width for the part. let mut pct = Table::new(3, 3, 300.0); (pct.props.width, pct.props.width_pct) = (None, Some(50.0)); let part = pct.slice((0, 2), (2, 2)); assert_eq!((part.props.width, part.props.width_pct), (Some(100.0), None)); // Nothing in range: no rows. assert!(t.slice((5, 6), (0, 2)).rows.is_empty()); assert!(t.slice((0, 2), (9, 9)).rows.is_empty()); } #[test] fn grid_before_shifts_the_row() { let mut t = Table::new(2, 3, 300.0); t.rows[1].cells.remove(0); t.rows[1].props.grid_before = 1; assert_eq!(t.grid_col(1, 0), 1); assert_eq!(t.grid_col(1, 1), 2); assert_eq!(t.cell_at_grid(1, 0), None); assert_eq!(t.cell_at_grid(1, 2), Some(1)); assert_eq!(t.cols(), 3); } #[test] fn new_table_shape() { let t = Table::new(3, 4, 468.0); assert_eq!(t.rows.len(), 3); assert_eq!(t.cols(), 4); assert_eq!(t.total_width(), 468.0); let huge = Table::new(1_000_000, 1000, 100.0); assert_eq!(huge.rows.len(), MAX_ROWS); assert_eq!(huge.cols(), MAX_COLS); } #[test] fn row_col_ops() { let mut t = Table::new(2, 2, 200.0); t.insert_row(1, 0); assert_eq!(t.rows.len(), 3); t.insert_col(1); assert_eq!(t.cols(), 3); assert!(t.rows.iter().all(|r| r.cells.len() == 3)); t.delete_col(0); assert_eq!(t.cols(), 2); t.delete_row(0); assert_eq!(t.rows.len(), 2); t.delete_row(99); t.delete_col(99); } /// #3637: a `.json` file can give a cell any span; inserting a column through it, merging it /// or deleting a column under it doesn't overflow, and the span it gets is in 1..=MAX_COLS. #[test] fn a_huge_span_from_a_file_does_not_overflow() { let huge = || { let mut t = Table::new(1, 2, 200.0); t.rows[0].cells[0].props.span = u32::MAX; t }; // Column 1 is inside the spanning cell, so the cell widens. let mut t = huge(); t.insert_col(1); assert_eq!(t.rows[0].cells[0].props.span, MAX_COLS as u32); let mut t = huge(); t.merge_right(0, 0, 1); assert_eq!(t.rows[0].cells.len(), 1); assert_eq!(t.rows[0].cells[0].props.span, MAX_COLS as u32); let mut t = huge(); t.rows[0].cells[1].props.span = u32::MAX; t.merge_right(0, 0, 1); assert_eq!(t.rows[0].cells[0].props.span, MAX_COLS as u32); let mut t = huge(); t.delete_col(0); assert_eq!(t.rows[0].cells[0].props.span, MAX_COLS as u32 - 1); } #[test] fn merge_and_split() { let mut t = Table::new(2, 3, 300.0); t.merge_right(0, 0, 1); assert_eq!(t.rows[0].cells.len(), 2); assert_eq!(t.rows[0].cells[0].span(), 2); assert_eq!(t.cell_at_grid(0, 1), Some(0)); assert_eq!(t.grid_col(0, 1), 2); t.insert_col(1); assert_eq!(t.rows[0].cells[0].span(), 3); t.split_cell(0, 0, 3); assert_eq!(t.rows[0].cells.len(), 4); let mut v = Table::new(3, 2, 100.0); v.merge(0, 2, 0, 0); assert_eq!(v.rows[0].cells[0].props.vmerge, VMerge::Restart); assert_eq!(v.rows[2].cells[0].props.vmerge, VMerge::Continue); } }