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(
246 grid: Option<&CT_TblGrid>,
247 available_width: f64,
248 tbl: &CT_Tbl,
249) -> Vec<f64> {
250 match grid {
251 Some(g) if !g.columns.is_empty() => {
252 let widths: Vec<f64> = g.columns.iter().map(|c| c.width.to_pt()).collect();
253 let total: f64 = widths.iter().sum();
254 if total > 0.01 && (total - available_width).abs() > 1.0 {
255 let scale = available_width / total;
257 widths.iter().map(|w| w * scale).collect()
258 } else if total < 0.01 {
259 let n = g.columns.len();
261 vec![available_width / n as f64; n]
262 } else {
263 widths
264 }
265 }
266 _ => {
267 let num_cols = tbl
269 .rows
270 .first()
271 .map(|r| {
272 r.cells
273 .iter()
274 .map(|c| {
275 c.properties.as_ref().and_then(|p| p.grid_span).unwrap_or(1) as usize
276 })
277 .sum::<usize>()
278 })
279 .unwrap_or(1)
280 .max(1);
281 vec![available_width / num_cols as f64; num_cols]
282 }
283 }
284}
285
286fn layout_cell_content(
291 content: &[rdocx_oxml::table::CellContent],
292 available_width: f64,
293 styles: &CT_Styles,
294 input: &LayoutInput,
295 fm: &mut FontManager,
296 num_state: &mut NumberingState,
297) -> Result<Vec<ParagraphBlock>> {
298 use crate::engine;
299 use rdocx_oxml::table::CellContent;
300
301 let mut blocks = Vec::new();
302 for item in content {
303 match item {
304 CellContent::Paragraph(para) => {
305 let block =
306 engine::layout_paragraph(para, available_width, styles, input, fm, num_state)?;
307 blocks.push(block);
308 }
309 CellContent::Table(tbl) => {
310 let _nested = layout_table(tbl, available_width, styles, input, fm, num_state)?;
312 for row in &_nested.rows {
315 for cell in &row.cells {
316 if !cell.is_vmerge_continue {
317 blocks.extend(cell.paragraphs.iter().cloned());
318 }
319 }
320 }
321 }
322 }
323 }
324 Ok(blocks)
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use rdocx_oxml::table::{CT_TblGrid, CT_TblGridCol};
331 use rdocx_oxml::units::Twips;
332
333 #[test]
334 fn column_widths_from_grid() {
335 let tbl = CT_Tbl::new();
336 let grid = CT_TblGrid {
337 columns: vec![
338 CT_TblGridCol { width: Twips(2880) }, CT_TblGridCol { width: Twips(2880) },
340 ],
341 };
342 let widths = compute_column_widths(Some(&grid), 468.0, &tbl);
343 assert_eq!(widths.len(), 2);
344 let total: f64 = widths.iter().sum();
346 assert!((total - 468.0).abs() < 1.0);
347 }
348
349 #[test]
350 fn column_widths_no_grid() {
351 let tbl = CT_Tbl::new();
352 let widths = compute_column_widths(None, 468.0, &tbl);
353 assert_eq!(widths.len(), 1);
354 assert!((widths[0] - 468.0).abs() < 0.01);
355 }
356
357 #[test]
358 fn column_widths_zero_grid() {
359 let tbl = CT_Tbl::new();
360 let grid = CT_TblGrid {
361 columns: vec![
362 CT_TblGridCol { width: Twips(0) },
363 CT_TblGridCol { width: Twips(0) },
364 CT_TblGridCol { width: Twips(0) },
365 ],
366 };
367 let widths = compute_column_widths(Some(&grid), 468.0, &tbl);
368 assert_eq!(widths.len(), 3);
369 for w in &widths {
370 assert!((w - 156.0).abs() < 0.01);
371 }
372 }
373
374 #[test]
375 fn column_widths_inferred_from_rows() {
376 use rdocx_oxml::table::{CT_Row, CT_Tc};
377 let mut tbl = CT_Tbl::new();
378 let mut row = CT_Row::new();
379 row.cells.push(CT_Tc::new());
380 row.cells.push(CT_Tc::new());
381 row.cells.push(CT_Tc::new());
382 tbl.rows.push(row);
383 let widths = compute_column_widths(None, 300.0, &tbl);
384 assert_eq!(widths.len(), 3);
385 for w in &widths {
386 assert!((w - 100.0).abs() < 0.01);
387 }
388 }
389
390 #[test]
391 fn nested_table_layout_dimensions() {
392 use rdocx_oxml::table::{CT_Row, CT_Tbl, CT_Tc, CellContent};
393
394 let mut outer = CT_Tbl::new();
396 outer.grid = Some(CT_TblGrid {
397 columns: vec![CT_TblGridCol { width: Twips(4680) }], });
399
400 let mut outer_row = CT_Row::new();
401 let mut outer_cell = CT_Tc::new();
402 outer_cell.paragraphs_mut()[0].add_run("Before nested");
403
404 let mut nested = CT_Tbl::new();
406 nested.grid = Some(CT_TblGrid {
407 columns: vec![
408 CT_TblGridCol { width: Twips(2000) },
409 CT_TblGridCol { width: Twips(2000) },
410 ],
411 });
412 let mut nr = CT_Row::new();
413 let mut nc1 = CT_Tc::new();
414 nc1.paragraphs_mut()[0].add_run("N1");
415 let mut nc2 = CT_Tc::new();
416 nc2.paragraphs_mut()[0].add_run("N2");
417 nr.cells.push(nc1);
418 nr.cells.push(nc2);
419 nested.rows.push(nr);
420
421 outer_cell.content.push(CellContent::Table(nested));
422 outer_row.cells.push(outer_cell);
423 outer.rows.push(outer_row);
424
425 let styles = rdocx_oxml::styles::CT_Styles::default();
427 let input = crate::input::LayoutInput {
428 document: rdocx_oxml::document::CT_Document {
429 body: rdocx_oxml::document::CT_Body {
430 content: Vec::new(),
431 sect_pr: None,
432 },
433 extra_namespaces: Vec::new(),
434 background_xml: None,
435 },
436 styles: styles.clone(),
437 numbering: None,
438 headers: std::collections::HashMap::new(),
439 footers: std::collections::HashMap::new(),
440 images: std::collections::HashMap::new(),
441 hyperlink_urls: std::collections::HashMap::new(),
442 footnotes: None,
443 endnotes: None,
444 core_properties: None,
445 theme: None,
446 fonts: Vec::new(),
447 };
448
449 let mut fm = crate::font::FontManager::new();
450 let mut num_state = crate::style_resolver::NumberingState::new();
451
452 let result = layout_table(&outer, 234.0, &styles, &input, &mut fm, &mut num_state);
453 assert!(result.is_ok());
454 let block = result.unwrap();
455
456 assert_eq!(block.rows.len(), 1);
458 assert_eq!(block.rows[0].cells.len(), 1);
459
460 let cell = &block.rows[0].cells[0];
462 assert!(
464 cell.paragraphs.len() >= 3,
465 "Expected at least 3 paragraph blocks from outer + nested content, got {}",
466 cell.paragraphs.len()
467 );
468
469 assert!((block.table_width - 234.0).abs() < 1.0);
471 }
472}