diff --git a/crates/doc/src/table.rs b/crates/doc/src/table.rs index d3c028067..76911dd4f 100644 --- a/crates/doc/src/table.rs +++ b/crates/doc/src/table.rs @@ -48,6 +48,12 @@ impl Row { 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)] @@ -97,7 +103,8 @@ impl Table { } pub fn cols(&self) -> usize { - let row_cols = |r: &Row| r.first_grid() + r.cells.iter().map(Cell::span).sum::() + r.props.grid_after as 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)) } diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index f639078ec..448182990 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -4967,6 +4967,8 @@ mod tests_autospace; #[cfg(test)] mod tests_bidi; #[cfg(test)] +mod tests_budget; +#[cfg(test)] mod tests_complex; #[cfg(test)] mod tests_grid; diff --git a/crates/layout/src/table.rs b/crates/layout/src/table.rs index d2e24e891..169eda6b9 100644 --- a/crates/layout/src/table.rs +++ b/crates/layout/src/table.rs @@ -537,7 +537,7 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava let row_borders = row.props.exceptions.as_ref().and_then(|e| e.borders).unwrap_or_default(); // A row that skips grid columns (`w:gridBefore`/`w:gridAfter`) has its edge at its own // first and last cell. - let (first_g, last_g) = (g == row.first_grid(), g + span >= ncols.saturating_sub(row.props.grid_after as usize)); + let (first_g, last_g) = (g == row.first_grid(), g + span >= ncols.saturating_sub(row.grid_after())); let rb = look.map(|l| region_borders(&l.borders, (ri, nrows), (first_g, last_g))).unwrap_or_default(); let mut borders = Borders { top: edge(cb.top.or(if ri == 0 { row_borders.top } else { row_borders.between }).or(rb.top), ri == 0, tb.top, tb.between), @@ -1170,7 +1170,7 @@ fn column_content(ctx: &mut Ctx, t: &Table, style: Option<&TableStyleProps>, dep /// columns, width in points). Column widths make room for them like for an empty cell. fn row_gaps(row: &Row, end: usize) -> impl Iterator { let p = &row.props; - [(0, row.first_grid(), p.w_before), (end, p.grid_after as usize, p.w_after)] + [(0, row.first_grid(), p.w_before), (end, row.grid_after(), p.w_after)] .into_iter() .filter(|&(_, n, _)| n > 0) .filter_map(|(g, n, w)| w.filter(|w| w.is_finite() && *w > 0.0).map(|w| (g, n, w.min(1584.0)))) diff --git a/crates/layout/src/tests_budget.rs b/crates/layout/src/tests_budget.rs new file mode 100644 index 000000000..c98efc3d6 --- /dev/null +++ b/crates/layout/src/tests_budget.rs @@ -0,0 +1,24 @@ +//! Hostile documents whose layout work or memory would otherwise grow far past their size: each +//! is bounded by a cap, and real documents stay well under it. + +use super::*; +use wordcraft_doc::Block; +use wordcraft_doc::table::{MAX_COLS, Table}; + +fn lay(doc: &Document) -> DocLayout { + let mut c = LayoutCache::new(); + layout(doc, &mut c, &LayoutOptions::default()) +} + +#[test] +fn a_huge_grid_after_is_bounded_like_the_docx_readers() { + // A native document can say a row leaves four billion columns empty after its cells. + let mut t = Table::new(1, 2, 468.0); + t.rows[0].props.grid_after = u32::MAX; + t.grid.clear(); + assert!(t.cols() <= 2 + MAX_COLS, "{} columns", t.cols()); + let mut d = Document::from_text(""); + d.insert_block(StoryRef::Body, &Path::top(1), Block::Table(t)).unwrap(); + let l = lay(&d); + assert!(l.pages[0].items.iter().any(|i| matches!(i, Placed::Cell { row: 0, cell: 1, .. }))); +}