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::input::{LayoutInput, MediaRegistry};
8use crate::style_resolver::NumberingState;
9use oxml_layout::{Color, FontManager, Result};
10
11#[derive(Debug, Clone)]
13pub struct TableBlock {
14 pub col_widths: Vec<f64>,
16 pub rows: Vec<TableRow>,
18 pub header_row_indices: Vec<usize>,
20 pub table_width: f64,
22 pub table_indent: f64,
24 pub borders: Option<CT_TblBorders>,
26}
27
28impl TableBlock {
29 pub fn content_height(&self) -> f64 {
31 self.rows.iter().map(|r| r.height).sum()
32 }
33
34 pub fn total_height(&self) -> f64 {
36 self.content_height()
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct TableRow {
43 pub cells: Vec<TableCell>,
45 pub height: f64,
47 pub is_header: bool,
49}
50
51#[derive(Debug, Clone)]
53pub struct TableCell {
54 pub paragraphs: Vec<ParagraphBlock>,
56 pub width: f64,
58 pub height: f64,
60 pub grid_span: u32,
62 pub is_vmerge_continue: bool,
64 pub col_index: usize,
66 pub borders: Option<CT_TblBorders>,
68 pub shading: Option<Color>,
70 pub margin_left: f64,
72 pub margin_top: f64,
74 pub is_first_row: bool,
76 pub is_last_row: bool,
78 pub v_align: Option<ST_VerticalJc>,
80}
81
82pub fn layout_table(
84 tbl: &CT_Tbl,
85 available_width: f64,
86 styles: &CT_Styles,
87 input: &LayoutInput,
88 media: &MediaRegistry,
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| 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(
185 &cell.content,
186 content_width,
187 styles,
188 input,
189 media,
190 fm,
191 num_state,
192 )?
193 };
194
195 let content_height: f64 = paragraphs.iter().map(|p| p.total_height()).sum::<f64>()
196 + cell_margin_top
197 + cell_margin_bottom;
198
199 let v_align = cell.properties.as_ref().and_then(|p| p.v_align);
200
201 cells.push(TableCell {
202 paragraphs,
203 width: cell_width,
204 height: content_height,
205 grid_span,
206 is_vmerge_continue,
207 col_index,
208 borders: cell_borders,
209 shading: cell_shading,
210 margin_left: cell_margin_left,
211 margin_top: cell_margin_top,
212 is_first_row: row_idx == 0,
213 is_last_row: row_idx == num_rows - 1,
214 v_align,
215 });
216
217 col_index += grid_span as usize;
218 }
219
220 let max_cell_height = cells.iter().map(|c| c.height).fold(0.0f64, f64::max);
222 let specified_height = row
223 .properties
224 .as_ref()
225 .and_then(|p| p.height)
226 .map(|h| h.to_pt())
227 .unwrap_or(0.0);
228 let row_height = max_cell_height.max(specified_height);
229
230 for cell in &mut cells {
232 cell.height = row_height;
233 }
234
235 rows.push(TableRow {
236 cells,
237 height: row_height,
238 is_header,
239 });
240 }
241
242 Ok(TableBlock {
243 col_widths,
244 rows,
245 header_row_indices,
246 table_width,
247 table_indent,
248 borders: table_borders,
249 })
250}
251
252fn compute_column_widths(
259 grid: Option<&CT_TblGrid>,
260 available_width: f64,
261 tbl: &CT_Tbl,
262) -> Vec<f64> {
263 match grid {
264 Some(g) if !g.columns.is_empty() => {
265 let widths: Vec<f64> = g.columns.iter().map(|c| c.width.to_pt()).collect();
266 let total: f64 = widths.iter().sum();
267 if total < 0.01 {
268 let n = g.columns.len();
270 vec![available_width / n as f64; n]
271 } else if total > available_width + 1.0 {
272 let scale = available_width / total;
274 widths.iter().map(|w| w * scale).collect()
275 } else {
276 widths
277 }
278 }
279 _ => {
280 let num_cols = tbl
282 .rows
283 .first()
284 .map(|r| {
285 r.cells
286 .iter()
287 .map(|c| {
288 c.properties.as_ref().and_then(|p| p.grid_span).unwrap_or(1) as usize
289 })
290 .sum::<usize>()
291 })
292 .unwrap_or(1)
293 .max(1);
294 vec![available_width / num_cols as f64; num_cols]
295 }
296 }
297}
298
299fn layout_cell_content(
304 content: &[rdocx_oxml::table::CellContent],
305 available_width: f64,
306 styles: &CT_Styles,
307 input: &LayoutInput,
308 media: &MediaRegistry,
309 fm: &mut FontManager,
310 num_state: &mut NumberingState,
311) -> Result<Vec<ParagraphBlock>> {
312 use crate::engine;
313 use rdocx_oxml::table::CellContent;
314
315 let mut blocks = Vec::new();
316 for item in content {
317 match item {
318 CellContent::Paragraph(para) => {
319 let block = engine::layout_paragraph(
320 para,
321 available_width,
322 styles,
323 input,
324 media,
325 fm,
326 num_state,
327 )?;
328 blocks.push(block);
329 }
330 CellContent::Table(tbl) => {
331 let _nested =
333 layout_table(tbl, available_width, styles, input, media, fm, num_state)?;
334 for row in &_nested.rows {
337 for cell in &row.cells {
338 if !cell.is_vmerge_continue {
339 blocks.extend(cell.paragraphs.iter().cloned());
340 }
341 }
342 }
343 }
344 }
345 }
346 Ok(blocks)
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use rdocx_oxml::table::{CT_TblGrid, CT_TblGridCol};
353 use rdocx_oxml::units::Twips;
354
355 #[test]
356 fn narrow_grid_keeps_its_declared_width() {
357 let tbl = CT_Tbl::new();
358 let grid = CT_TblGrid {
359 columns: vec![
360 CT_TblGridCol { width: Twips(2880) }, CT_TblGridCol { width: Twips(2880) },
362 ],
363 };
364
365 let widths = compute_column_widths(Some(&grid), 468.0, &tbl);
368
369 assert_eq!(widths.len(), 2);
370 let total: f64 = widths.iter().sum();
371 assert!((total - 288.0).abs() < 1.0, "got {total}");
372 }
373
374 #[test]
375 fn overflowing_grid_is_scaled_down_to_fit() {
376 let tbl = CT_Tbl::new();
377 let grid = CT_TblGrid {
378 columns: vec![
379 CT_TblGridCol { width: Twips(7200) }, CT_TblGridCol { width: Twips(7200) },
381 ],
382 };
383
384 let widths = compute_column_widths(Some(&grid), 468.0, &tbl);
386
387 let total: f64 = widths.iter().sum();
388 assert!((total - 468.0).abs() < 1.0, "got {total}");
389 assert!((widths[0] - widths[1]).abs() < 0.01);
391 }
392
393 #[test]
394 fn column_widths_no_grid() {
395 let tbl = CT_Tbl::new();
396 let widths = compute_column_widths(None, 468.0, &tbl);
397 assert_eq!(widths.len(), 1);
398 assert!((widths[0] - 468.0).abs() < 0.01);
399 }
400
401 #[test]
402 fn column_widths_zero_grid() {
403 let tbl = CT_Tbl::new();
404 let grid = CT_TblGrid {
405 columns: vec![
406 CT_TblGridCol { width: Twips(0) },
407 CT_TblGridCol { width: Twips(0) },
408 CT_TblGridCol { width: Twips(0) },
409 ],
410 };
411 let widths = compute_column_widths(Some(&grid), 468.0, &tbl);
412 assert_eq!(widths.len(), 3);
413 for w in &widths {
414 assert!((w - 156.0).abs() < 0.01);
415 }
416 }
417
418 #[test]
419 fn column_widths_inferred_from_rows() {
420 use rdocx_oxml::table::{CT_Row, CT_Tc};
421 let mut tbl = CT_Tbl::new();
422 let mut row = CT_Row::new();
423 row.cells.push(CT_Tc::new());
424 row.cells.push(CT_Tc::new());
425 row.cells.push(CT_Tc::new());
426 tbl.rows.push(row);
427 let widths = compute_column_widths(None, 300.0, &tbl);
428 assert_eq!(widths.len(), 3);
429 for w in &widths {
430 assert!((w - 100.0).abs() < 0.01);
431 }
432 }
433
434 #[test]
435 fn nested_table_layout_dimensions() {
436 use rdocx_oxml::table::{CT_Row, CT_Tbl, CT_Tc, CellContent};
437
438 let mut outer = CT_Tbl::new();
440 outer.grid = Some(CT_TblGrid {
441 columns: vec![CT_TblGridCol { width: Twips(4680) }], });
443
444 let mut outer_row = CT_Row::new();
445 let mut outer_cell = CT_Tc::new();
446 outer_cell.paragraphs_mut()[0].add_run("Before nested");
447
448 let mut nested = CT_Tbl::new();
450 nested.grid = Some(CT_TblGrid {
451 columns: vec![
452 CT_TblGridCol { width: Twips(2000) },
453 CT_TblGridCol { width: Twips(2000) },
454 ],
455 });
456 let mut nr = CT_Row::new();
457 let mut nc1 = CT_Tc::new();
458 nc1.paragraphs_mut()[0].add_run("N1");
459 let mut nc2 = CT_Tc::new();
460 nc2.paragraphs_mut()[0].add_run("N2");
461 nr.cells.push(nc1);
462 nr.cells.push(nc2);
463 nested.rows.push(nr);
464
465 outer_cell.content.push(CellContent::Table(nested));
466 outer_row.cells.push(outer_cell);
467 outer.rows.push(outer_row);
468
469 let styles = rdocx_oxml::styles::CT_Styles::default();
471 let input = crate::input::LayoutInput {
472 document: rdocx_oxml::document::CT_Document {
473 body: rdocx_oxml::document::CT_Body {
474 content: Vec::new(),
475 sect_pr: None,
476 },
477 extra_namespaces: Vec::new(),
478 background_xml: None,
479 },
480 styles: styles.clone(),
481 numbering: None,
482 headers: std::collections::HashMap::new(),
483 footers: std::collections::HashMap::new(),
484 images: std::collections::HashMap::new(),
485 hyperlink_urls: std::collections::HashMap::new(),
486 footnotes: None,
487 endnotes: None,
488 core_properties: None,
489 theme: None,
490 fonts: Vec::new(),
491 };
492
493 let mut fm = FontManager::new();
494 let mut num_state = crate::style_resolver::NumberingState::new();
495 let media = MediaRegistry::new(&input.images);
496
497 let result = layout_table(
498 &outer,
499 234.0,
500 &styles,
501 &input,
502 &media,
503 &mut fm,
504 &mut num_state,
505 );
506 assert!(result.is_ok());
507 let block = result.unwrap();
508
509 assert_eq!(block.rows.len(), 1);
511 assert_eq!(block.rows[0].cells.len(), 1);
512
513 let cell = &block.rows[0].cells[0];
515 assert!(
517 cell.paragraphs.len() >= 3,
518 "Expected at least 3 paragraph blocks from outer + nested content, got {}",
519 cell.paragraphs.len()
520 );
521
522 assert!((block.table_width - 234.0).abs() < 1.0);
524 }
525}