1use rdocx_oxml::styles::CT_Styles;
4use rdocx_oxml::table::{CT_Tbl, CT_TblBorders, CT_TblGrid, ST_VerticalJc, VMerge};
5
6use crate::block::ParagraphBlock;
7use crate::error::Result;
8use crate::font::FontManager;
9use crate::input::LayoutInput;
10use crate::style_resolver::NumberingState;
11
12#[derive(Debug, Clone)]
14pub struct TableBlock {
15 pub col_widths: Vec<f64>,
17 pub rows: Vec<TableRow>,
19 pub header_row_indices: Vec<usize>,
21 pub table_width: f64,
23 pub table_indent: f64,
25 pub borders: Option<CT_TblBorders>,
27}
28
29impl TableBlock {
30 pub fn content_height(&self) -> f64 {
32 self.rows.iter().map(|r| r.height).sum()
33 }
34
35 pub fn total_height(&self) -> f64 {
37 self.content_height()
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct TableRow {
44 pub cells: Vec<TableCell>,
46 pub height: f64,
48 pub is_header: bool,
50}
51
52#[derive(Debug, Clone)]
54pub struct TableCell {
55 pub paragraphs: Vec<ParagraphBlock>,
57 pub width: f64,
59 pub height: f64,
61 pub grid_span: u32,
63 pub is_vmerge_continue: bool,
65 pub col_index: usize,
67 pub borders: Option<CT_TblBorders>,
69 pub shading: Option<crate::output::Color>,
71 pub margin_left: f64,
73 pub margin_top: f64,
75 pub is_first_row: bool,
77 pub is_last_row: bool,
79 pub v_align: Option<ST_VerticalJc>,
81}
82
83pub fn layout_table(
85 tbl: &CT_Tbl,
86 available_width: f64,
87 styles: &CT_Styles,
88 input: &LayoutInput,
89 fm: &mut FontManager,
90 num_state: &mut NumberingState,
91) -> Result<TableBlock> {
92 let col_widths = compute_column_widths(tbl.grid.as_ref(), available_width, tbl);
94 let table_width: f64 = col_widths.iter().sum();
95
96 let table_indent = tbl
98 .properties
99 .as_ref()
100 .and_then(|p| p.indent.as_ref())
101 .map(|ind| {
102 if ind.width_type == "dxa" {
103 ind.w as f64 / 20.0 } else {
105 0.0
106 }
107 })
108 .unwrap_or(0.0);
109
110 let table_borders = tbl.properties.as_ref().and_then(|p| p.borders.clone());
112
113 let default_cell_margin = tbl.properties.as_ref().and_then(|p| p.cell_margin.as_ref());
115 let cell_margin_left = default_cell_margin
116 .and_then(|m| m.left)
117 .map(|t| t.to_pt())
118 .unwrap_or(5.4); let cell_margin_right = default_cell_margin
120 .and_then(|m| m.right)
121 .map(|t| t.to_pt())
122 .unwrap_or(5.4);
123 let cell_margin_top = default_cell_margin
124 .and_then(|m| m.top)
125 .map(|t| t.to_pt())
126 .unwrap_or(0.0);
127 let cell_margin_bottom = default_cell_margin
128 .and_then(|m| m.bottom)
129 .map(|t| t.to_pt())
130 .unwrap_or(0.0);
131
132 let num_rows = tbl.rows.len();
133 let mut header_row_indices = Vec::new();
134 let mut rows = Vec::new();
135
136 for (row_idx, row) in tbl.rows.iter().enumerate() {
137 let is_header = row
138 .properties
139 .as_ref()
140 .and_then(|p| p.header)
141 .unwrap_or(false);
142 if is_header {
143 header_row_indices.push(row_idx);
144 }
145
146 let mut cells = Vec::new();
147 let mut col_index = 0usize;
148
149 for cell in &row.cells {
150 let grid_span = cell
151 .properties
152 .as_ref()
153 .and_then(|p| p.grid_span)
154 .unwrap_or(1);
155
156 let is_vmerge_continue = cell
157 .properties
158 .as_ref()
159 .and_then(|p| p.v_merge)
160 .map(|vm| vm == VMerge::Continue)
161 .unwrap_or(false);
162
163 let cell_borders = cell.properties.as_ref().and_then(|p| p.borders.clone());
165 let cell_shading = cell
166 .properties
167 .as_ref()
168 .and_then(|p| p.shading.as_ref())
169 .and_then(|shd| shd.fill.as_ref())
170 .filter(|f| f.as_str() != "auto")
171 .map(|f| crate::output::Color::from_hex(f));
172
173 let cell_width: f64 = (col_index..col_index + grid_span as usize)
175 .filter_map(|i| col_widths.get(i))
176 .sum();
177
178 let content_width = (cell_width - cell_margin_left - cell_margin_right).max(0.0);
179
180 let paragraphs = if is_vmerge_continue {
182 Vec::new()
183 } else {
184 layout_cell_content(&cell.content, content_width, styles, input, fm, num_state)?
185 };
186
187 let content_height: f64 = paragraphs.iter().map(|p| p.total_height()).sum::<f64>()
188 + cell_margin_top
189 + cell_margin_bottom;
190
191 let v_align = cell.properties.as_ref().and_then(|p| p.v_align);
192
193 cells.push(TableCell {
194 paragraphs,
195 width: cell_width,
196 height: content_height,
197 grid_span,
198 is_vmerge_continue,
199 col_index,
200 borders: cell_borders,
201 shading: cell_shading,
202 margin_left: cell_margin_left,
203 margin_top: cell_margin_top,
204 is_first_row: row_idx == 0,
205 is_last_row: row_idx == num_rows - 1,
206 v_align,
207 });
208
209 col_index += grid_span as usize;
210 }
211
212 let max_cell_height = cells.iter().map(|c| c.height).fold(0.0f64, f64::max);
214 let specified_height = row
215 .properties
216 .as_ref()
217 .and_then(|p| p.height)
218 .map(|h| h.to_pt())
219 .unwrap_or(0.0);
220 let row_height = max_cell_height.max(specified_height);
221
222 for cell in &mut cells {
224 cell.height = row_height;
225 }
226
227 rows.push(TableRow {
228 cells,
229 height: row_height,
230 is_header,
231 });
232 }
233
234 Ok(TableBlock {
235 col_widths,
236 rows,
237 header_row_indices,
238 table_width,
239 table_indent,
240 borders: table_borders,
241 })
242}
243
244fn compute_column_widths(
251 grid: Option<&CT_TblGrid>,
252 available_width: f64,
253 tbl: &CT_Tbl,
254) -> Vec<f64> {
255 match grid {
256 Some(g) if !g.columns.is_empty() => {
257 let widths: Vec<f64> = g.columns.iter().map(|c| c.width.to_pt()).collect();
258 let total: f64 = widths.iter().sum();
259 if total < 0.01 {
260 let n = g.columns.len();
262 vec![available_width / n as f64; n]
263 } else if total > available_width + 1.0 {
264 let scale = available_width / total;
266 widths.iter().map(|w| w * scale).collect()
267 } else {
268 widths
269 }
270 }
271 _ => {
272 let num_cols = tbl
274 .rows
275 .first()
276 .map(|r| {
277 r.cells
278 .iter()
279 .map(|c| {
280 c.properties.as_ref().and_then(|p| p.grid_span).unwrap_or(1) as usize
281 })
282 .sum::<usize>()
283 })
284 .unwrap_or(1)
285 .max(1);
286 vec![available_width / num_cols as f64; num_cols]
287 }
288 }
289}
290
291fn layout_cell_content(
296 content: &[rdocx_oxml::table::CellContent],
297 available_width: f64,
298 styles: &CT_Styles,
299 input: &LayoutInput,
300 fm: &mut FontManager,
301 num_state: &mut NumberingState,
302) -> Result<Vec<ParagraphBlock>> {
303 use crate::engine;
304 use rdocx_oxml::table::CellContent;
305
306 let mut blocks = Vec::new();
307 for item in content {
308 match item {
309 CellContent::Paragraph(para) => {
310 let block =
311 engine::layout_paragraph(para, available_width, styles, input, fm, num_state)?;
312 blocks.push(block);
313 }
314 CellContent::Table(tbl) => {
315 let _nested = layout_table(tbl, available_width, styles, input, fm, num_state)?;
317 for row in &_nested.rows {
320 for cell in &row.cells {
321 if !cell.is_vmerge_continue {
322 blocks.extend(cell.paragraphs.iter().cloned());
323 }
324 }
325 }
326 }
327 }
328 }
329 Ok(blocks)
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use rdocx_oxml::table::{CT_TblGrid, CT_TblGridCol};
336 use rdocx_oxml::units::Twips;
337
338 #[test]
339 fn narrow_grid_keeps_its_declared_width() {
340 let tbl = CT_Tbl::new();
341 let grid = CT_TblGrid {
342 columns: vec![
343 CT_TblGridCol { width: Twips(2880) }, CT_TblGridCol { width: Twips(2880) },
345 ],
346 };
347
348 let widths = compute_column_widths(Some(&grid), 468.0, &tbl);
351
352 assert_eq!(widths.len(), 2);
353 let total: f64 = widths.iter().sum();
354 assert!((total - 288.0).abs() < 1.0, "got {total}");
355 }
356
357 #[test]
358 fn overflowing_grid_is_scaled_down_to_fit() {
359 let tbl = CT_Tbl::new();
360 let grid = CT_TblGrid {
361 columns: vec![
362 CT_TblGridCol { width: Twips(7200) }, CT_TblGridCol { width: Twips(7200) },
364 ],
365 };
366
367 let widths = compute_column_widths(Some(&grid), 468.0, &tbl);
369
370 let total: f64 = widths.iter().sum();
371 assert!((total - 468.0).abs() < 1.0, "got {total}");
372 assert!((widths[0] - widths[1]).abs() < 0.01);
374 }
375
376 #[test]
377 fn column_widths_no_grid() {
378 let tbl = CT_Tbl::new();
379 let widths = compute_column_widths(None, 468.0, &tbl);
380 assert_eq!(widths.len(), 1);
381 assert!((widths[0] - 468.0).abs() < 0.01);
382 }
383
384 #[test]
385 fn column_widths_zero_grid() {
386 let tbl = CT_Tbl::new();
387 let grid = CT_TblGrid {
388 columns: vec![
389 CT_TblGridCol { width: Twips(0) },
390 CT_TblGridCol { width: Twips(0) },
391 CT_TblGridCol { width: Twips(0) },
392 ],
393 };
394 let widths = compute_column_widths(Some(&grid), 468.0, &tbl);
395 assert_eq!(widths.len(), 3);
396 for w in &widths {
397 assert!((w - 156.0).abs() < 0.01);
398 }
399 }
400
401 #[test]
402 fn column_widths_inferred_from_rows() {
403 use rdocx_oxml::table::{CT_Row, CT_Tc};
404 let mut tbl = CT_Tbl::new();
405 let mut row = CT_Row::new();
406 row.cells.push(CT_Tc::new());
407 row.cells.push(CT_Tc::new());
408 row.cells.push(CT_Tc::new());
409 tbl.rows.push(row);
410 let widths = compute_column_widths(None, 300.0, &tbl);
411 assert_eq!(widths.len(), 3);
412 for w in &widths {
413 assert!((w - 100.0).abs() < 0.01);
414 }
415 }
416
417 #[test]
418 fn nested_table_layout_dimensions() {
419 use rdocx_oxml::table::{CT_Row, CT_Tbl, CT_Tc, CellContent};
420
421 let mut outer = CT_Tbl::new();
423 outer.grid = Some(CT_TblGrid {
424 columns: vec![CT_TblGridCol { width: Twips(4680) }], });
426
427 let mut outer_row = CT_Row::new();
428 let mut outer_cell = CT_Tc::new();
429 outer_cell.paragraphs_mut()[0].add_run("Before nested");
430
431 let mut nested = CT_Tbl::new();
433 nested.grid = Some(CT_TblGrid {
434 columns: vec![
435 CT_TblGridCol { width: Twips(2000) },
436 CT_TblGridCol { width: Twips(2000) },
437 ],
438 });
439 let mut nr = CT_Row::new();
440 let mut nc1 = CT_Tc::new();
441 nc1.paragraphs_mut()[0].add_run("N1");
442 let mut nc2 = CT_Tc::new();
443 nc2.paragraphs_mut()[0].add_run("N2");
444 nr.cells.push(nc1);
445 nr.cells.push(nc2);
446 nested.rows.push(nr);
447
448 outer_cell.content.push(CellContent::Table(nested));
449 outer_row.cells.push(outer_cell);
450 outer.rows.push(outer_row);
451
452 let styles = rdocx_oxml::styles::CT_Styles::default();
454 let input = crate::input::LayoutInput {
455 document: rdocx_oxml::document::CT_Document {
456 body: rdocx_oxml::document::CT_Body {
457 content: Vec::new(),
458 sect_pr: None,
459 },
460 extra_namespaces: Vec::new(),
461 background_xml: None,
462 },
463 styles: styles.clone(),
464 numbering: None,
465 headers: std::collections::HashMap::new(),
466 footers: std::collections::HashMap::new(),
467 images: std::collections::HashMap::new(),
468 hyperlink_urls: std::collections::HashMap::new(),
469 footnotes: None,
470 endnotes: None,
471 core_properties: None,
472 theme: None,
473 fonts: Vec::new(),
474 };
475
476 let mut fm = crate::font::FontManager::new();
477 let mut num_state = crate::style_resolver::NumberingState::new();
478
479 let result = layout_table(&outer, 234.0, &styles, &input, &mut fm, &mut num_state);
480 assert!(result.is_ok());
481 let block = result.unwrap();
482
483 assert_eq!(block.rows.len(), 1);
485 assert_eq!(block.rows[0].cells.len(), 1);
486
487 let cell = &block.rows[0].cells[0];
489 assert!(
491 cell.paragraphs.len() >= 3,
492 "Expected at least 3 paragraph blocks from outer + nested content, got {}",
493 cell.paragraphs.len()
494 );
495
496 assert!((block.table_width - 234.0).abs() < 1.0);
498 }
499}