1use rdocx_oxml::content_control::{CT_Sdt, SdtContent};
4use rdocx_oxml::styles::CT_Styles;
5use rdocx_oxml::table::{CT_Row, CT_Tbl, CT_TblBorders, CT_TblGrid, CT_Tc, ST_VerticalJc, VMerge};
6
7use crate::WordStory;
8use crate::block::{
9 CellBlockSemantics, CellSemantics, ParagraphBlock, ParagraphSemantics, RowSemantics,
10 TableSemantics,
11};
12use crate::engine::SourceRegistry;
13use crate::input::{LayoutInput, MediaRegistry};
14use crate::style_resolver::NumberingState;
15use oxml_layout::{Color, Diagnostic, FontManager, Result, StructureId};
16
17const CONTROL_PATH_COMPONENT: usize = usize::MAX;
18
19pub(crate) fn layout_table_rows<'a>(
20 table: &'a CT_Tbl,
21 path: &[usize],
22) -> Vec<(&'a CT_Row, Vec<usize>)> {
23 let mut rows = Vec::new();
24 for boundary in 0..=table.rows.len() {
25 for (control_index, (_, _, control)) in table
26 .content_controls
27 .iter()
28 .enumerate()
29 .filter(|(_, (position, _, _))| *position == boundary)
30 {
31 let mut control_path = path.to_vec();
32 control_path.extend([CONTROL_PATH_COMPONENT, boundary, control_index]);
33 collect_control_rows(control, &control_path, &mut rows);
34 }
35 if let Some(row) = table.rows.get(boundary) {
36 let mut row_path = path.to_vec();
37 row_path.push(boundary);
38 rows.push((row, row_path));
39 }
40 }
41 rows
42}
43
44fn collect_control_rows<'a>(
45 control: &'a CT_Sdt,
46 path: &[usize],
47 rows: &mut Vec<(&'a CT_Row, Vec<usize>)>,
48) {
49 for (content_index, content) in control.content.iter().enumerate() {
50 let mut content_path = path.to_vec();
51 content_path.push(content_index);
52 match content {
53 SdtContent::Row(row) => rows.push((row, content_path)),
54 SdtContent::ContentControl(control) => {
55 collect_control_rows(control, &content_path, rows)
56 }
57 _ => {}
58 }
59 }
60}
61
62pub(crate) fn layout_row_cells<'a>(
63 row: &'a CT_Row,
64 path: &[usize],
65) -> Vec<(&'a CT_Tc, Vec<usize>)> {
66 let mut cells = Vec::new();
67 for boundary in 0..=row.cells.len() {
68 for (control_index, (_, _, control)) in row
69 .content_controls
70 .iter()
71 .enumerate()
72 .filter(|(_, (position, _, _))| *position == boundary)
73 {
74 let mut control_path = path.to_vec();
75 control_path.extend([CONTROL_PATH_COMPONENT, boundary, control_index]);
76 collect_control_cells(control, &control_path, &mut cells);
77 }
78 if let Some(cell) = row.cells.get(boundary) {
79 let mut cell_path = path.to_vec();
80 cell_path.push(boundary);
81 cells.push((cell, cell_path));
82 }
83 }
84 cells
85}
86
87fn collect_control_cells<'a>(
88 control: &'a CT_Sdt,
89 path: &[usize],
90 cells: &mut Vec<(&'a CT_Tc, Vec<usize>)>,
91) {
92 for (content_index, content) in control.content.iter().enumerate() {
93 let mut content_path = path.to_vec();
94 content_path.push(content_index);
95 match content {
96 SdtContent::Cell(cell) => cells.push((cell, content_path)),
97 SdtContent::ContentControl(control) => {
98 collect_control_cells(control, &content_path, cells)
99 }
100 _ => {}
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct TableBlock {
108 pub structure_id: Option<StructureId>,
110 pub col_widths: Vec<f64>,
112 pub rows: Vec<TableRow>,
114 pub header_row_indices: Vec<usize>,
116 pub table_width: f64,
118 pub table_indent: f64,
120 pub borders: Option<CT_TblBorders>,
122}
123
124impl TableBlock {
125 pub fn content_height(&self) -> f64 {
127 self.rows.iter().map(|r| r.height).sum()
128 }
129
130 pub fn total_height(&self) -> f64 {
132 self.content_height()
133 }
134}
135
136#[derive(Debug, Clone)]
138pub struct TableRow {
139 pub structure_id: Option<StructureId>,
141 pub cells: Vec<TableCell>,
143 pub height: f64,
145 pub is_header: bool,
147}
148
149#[derive(Debug, Clone)]
151pub enum CellBlock {
152 Paragraph(ParagraphBlock),
154 Table(TableBlock),
156}
157
158impl CellBlock {
159 pub fn total_height(&self) -> f64 {
161 match self {
162 Self::Paragraph(paragraph) => paragraph.total_height(),
163 Self::Table(table) => table.total_height(),
164 }
165 }
166}
167
168#[derive(Debug, Clone)]
170pub struct TableCell {
171 pub structure_id: Option<StructureId>,
173 pub blocks: Vec<CellBlock>,
175 pub width: f64,
177 pub height: f64,
179 pub grid_span: u32,
181 pub is_vmerge_continue: bool,
183 pub starts_vmerge: bool,
185 pub merged_height: f64,
187 pub merge_with_below: bool,
189 pub clip_content: bool,
191 pub col_index: usize,
193 pub borders: Option<CT_TblBorders>,
195 pub shading: Option<Color>,
197 pub margin_left: f64,
199 pub margin_right: f64,
201 pub margin_top: f64,
203 pub margin_bottom: f64,
205 pub is_first_row: bool,
207 pub is_last_row: bool,
209 pub v_align: Option<ST_VerticalJc>,
211}
212
213pub fn layout_table(
215 tbl: &CT_Tbl,
216 available_width: f64,
217 styles: &CT_Styles,
218 input: &LayoutInput,
219 media: &MediaRegistry,
220 fm: &mut FontManager,
221 num_state: &mut NumberingState,
222 diagnostics: &mut Vec<Diagnostic>,
223) -> Result<TableBlock> {
224 layout_table_inner(
225 tbl,
226 available_width,
227 styles,
228 input,
229 media,
230 fm,
231 num_state,
232 diagnostics,
233 None,
234 &WordStory::Document,
235 &[],
236 )
237 .map(|(block, _)| block)
238}
239
240pub(crate) fn layout_table_with_provenance(
241 tbl: &CT_Tbl,
242 available_width: f64,
243 styles: &CT_Styles,
244 input: &LayoutInput,
245 media: &MediaRegistry,
246 fm: &mut FontManager,
247 num_state: &mut NumberingState,
248 diagnostics: &mut Vec<Diagnostic>,
249 sources: Option<&SourceRegistry>,
250 story: &WordStory,
251 path: &[usize],
252) -> Result<(TableBlock, TableSemantics)> {
253 layout_table_inner(
254 tbl,
255 available_width,
256 styles,
257 input,
258 media,
259 fm,
260 num_state,
261 diagnostics,
262 sources,
263 story,
264 path,
265 )
266}
267
268fn layout_table_inner(
269 tbl: &CT_Tbl,
270 available_width: f64,
271 styles: &CT_Styles,
272 input: &LayoutInput,
273 media: &MediaRegistry,
274 fm: &mut FontManager,
275 num_state: &mut NumberingState,
276 diagnostics: &mut Vec<Diagnostic>,
277 sources: Option<&SourceRegistry>,
278 story: &WordStory,
279 path: &[usize],
280) -> Result<(TableBlock, TableSemantics)> {
281 let source_rows = layout_table_rows(tbl, path);
282 let col_widths = compute_column_widths(tbl.grid.as_ref(), available_width, tbl, path);
284 let table_width: f64 = col_widths.iter().sum();
285
286 let table_indent = tbl
288 .properties
289 .as_ref()
290 .and_then(|p| p.indent.as_ref())
291 .map(|ind| {
292 if ind.width_type == "dxa" {
293 ind.w as f64 / 20.0 } else {
295 0.0
296 }
297 })
298 .unwrap_or(0.0);
299
300 let table_borders = tbl
302 .properties
303 .as_ref()
304 .and_then(|properties| properties.borders.clone())
305 .or_else(|| {
306 let mut style_id = tbl.properties.as_ref()?.style_id.as_deref()?;
307 let mut visited = std::collections::HashSet::new();
308 while visited.insert(style_id) {
309 let style = styles.get_by_id(style_id)?;
310 if let Some(properties) = &style.table_properties
311 && let Some(borders) = &properties.borders
312 {
313 return Some(borders.clone());
314 }
315 style_id = style.based_on.as_deref()?;
316 }
317 None
318 });
319
320 let default_cell_margin = tbl.properties.as_ref().and_then(|p| p.cell_margin.as_ref());
322 let cell_margin_left = default_cell_margin
323 .and_then(|m| m.left)
324 .map(|t| t.to_pt())
325 .unwrap_or(5.4); let cell_margin_right = default_cell_margin
327 .and_then(|m| m.right)
328 .map(|t| t.to_pt())
329 .unwrap_or(5.4);
330 let cell_margin_top = default_cell_margin
331 .and_then(|m| m.top)
332 .map(|t| t.to_pt())
333 .unwrap_or(0.0);
334 let cell_margin_bottom = default_cell_margin
335 .and_then(|m| m.bottom)
336 .map(|t| t.to_pt())
337 .unwrap_or(0.0);
338
339 let num_rows = source_rows.len();
340 let mut header_row_indices = Vec::new();
341 let mut rows = Vec::new();
342 let mut row_semantics = Vec::new();
343 let mut exact_rows = Vec::new();
344
345 for (row_idx, (row, row_path)) in source_rows.iter().enumerate() {
346 let is_header = row
347 .properties
348 .as_ref()
349 .and_then(|p| p.header)
350 .unwrap_or(false);
351 if is_header {
352 header_row_indices.push(row_idx);
353 }
354
355 let mut cells = Vec::new();
356 let mut cell_semantics = Vec::new();
357 let mut col_index = 0usize;
358
359 let source_cells = layout_row_cells(row, row_path);
360 for (cell, cell_path) in &source_cells {
361 let grid_span = cell
362 .properties
363 .as_ref()
364 .and_then(|p| p.grid_span)
365 .unwrap_or(1);
366
367 let is_vmerge_continue = cell
368 .properties
369 .as_ref()
370 .and_then(|p| p.v_merge)
371 .map(|vm| vm == VMerge::Continue)
372 .unwrap_or(false);
373 let starts_vmerge =
374 cell.properties.as_ref().and_then(|p| p.v_merge) == Some(VMerge::Restart);
375
376 let style_cell = resolve_table_style_cell(
377 tbl,
378 styles,
379 row_idx,
380 col_index,
381 num_rows,
382 col_widths.len(),
383 row.properties
384 .as_ref()
385 .and_then(|properties| properties.cnf_style.as_deref()),
386 cell.properties
387 .as_ref()
388 .and_then(|properties| properties.cnf_style.as_deref()),
389 );
390
391 let mut cell_borders = style_cell.borders;
393 if let Some(direct) = cell.properties.as_ref().and_then(|p| p.borders.as_ref()) {
394 overlay_borders(&mut cell_borders, direct);
395 }
396 let cell_shading = cell
397 .properties
398 .as_ref()
399 .and_then(|p| p.shading.as_ref())
400 .or(style_cell.shading.as_ref())
401 .and_then(|shd| shd.fill.as_ref())
402 .filter(|f| f.as_str() != "auto")
403 .map(|f| Color::from_hex(f));
404
405 let cell_width: f64 = (col_index..col_index + grid_span as usize)
407 .filter_map(|i| col_widths.get(i))
408 .sum();
409
410 let content_width = (cell_width - cell_margin_left - cell_margin_right).max(0.0);
411
412 let (blocks, block_semantics) = if is_vmerge_continue {
414 (Vec::new(), Vec::new())
415 } else {
416 layout_cell_content(
417 &cell.content,
418 content_width,
419 styles,
420 input,
421 media,
422 fm,
423 num_state,
424 diagnostics,
425 sources,
426 story,
427 cell_path,
428 style_cell.paragraph_properties.as_ref(),
429 )?
430 };
431
432 let content_height: f64 = blocks.iter().map(CellBlock::total_height).sum::<f64>()
433 + cell_margin_top
434 + cell_margin_bottom;
435
436 let v_align = cell.properties.as_ref().and_then(|p| p.v_align);
437
438 cells.push(TableCell {
439 structure_id: None,
440 blocks,
441 width: cell_width,
442 height: content_height,
443 grid_span,
444 is_vmerge_continue,
445 starts_vmerge,
446 merged_height: content_height,
447 merge_with_below: false,
448 clip_content: false,
449 col_index,
450 borders: cell_borders,
451 shading: cell_shading,
452 margin_left: cell_margin_left,
453 margin_right: cell_margin_right,
454 margin_top: cell_margin_top,
455 margin_bottom: cell_margin_bottom,
456 is_first_row: row_idx == 0,
457 is_last_row: row_idx == num_rows - 1,
458 v_align,
459 });
460 cell_semantics.push(CellSemantics {
461 blocks: block_semantics,
462 });
463
464 col_index += grid_span as usize;
465 }
466
467 let max_cell_height = cells
468 .iter()
469 .filter(|cell| !cell.starts_vmerge)
470 .map(|cell| cell.height)
471 .fold(0.0f64, f64::max);
472 let specified_height = row
473 .properties
474 .as_ref()
475 .and_then(|p| p.height)
476 .map(|h| h.to_pt())
477 .unwrap_or(0.0);
478 let exact = row
479 .properties
480 .as_ref()
481 .and_then(|properties| properties.height_rule.as_deref())
482 == Some("exact")
483 && specified_height > 0.0;
484 exact_rows.push(exact);
485 for cell in &mut cells {
486 cell.clip_content = exact && !cell.is_vmerge_continue;
487 }
488 let row_height = if exact {
489 specified_height
490 } else {
491 max_cell_height.max(specified_height)
492 };
493
494 rows.push(TableRow {
495 structure_id: None,
496 cells,
497 height: row_height,
498 is_header,
499 });
500 row_semantics.push(RowSemantics {
501 cells: cell_semantics,
502 });
503 }
504
505 let mut spans = Vec::new();
508 for row_index in 0..rows.len() {
509 for cell_index in 0..rows[row_index].cells.len() {
510 let cell = &rows[row_index].cells[cell_index];
511 if !cell.starts_vmerge {
512 continue;
513 }
514 let start_col = cell.col_index;
515 let grid_span = cell.grid_span;
516 let mut last_row = row_index;
517 while let Some(next_row) = rows.get(last_row + 1) {
518 let continues = next_row.cells.iter().any(|next| {
519 next.is_vmerge_continue
520 && next.col_index == start_col
521 && next.grid_span == grid_span
522 });
523 if !continues {
524 break;
525 }
526 last_row += 1;
527 }
528 let required = cell.height;
529 let available = rows[row_index..=last_row]
530 .iter()
531 .map(|row| row.height)
532 .sum::<f64>();
533 if required > available
534 && let Some(grow_row) = (row_index..=last_row)
535 .rev()
536 .find(|candidate| !exact_rows[*candidate])
537 {
538 rows[grow_row].height += required - available;
539 }
540 spans.push((row_index, cell_index, last_row, required));
541 }
542 }
543
544 let row_heights = rows.iter().map(|row| row.height).collect::<Vec<_>>();
545 for row_index in 0..rows.len() {
546 let continuing_spans = rows
547 .get(row_index + 1)
548 .map(|next_row| {
549 next_row
550 .cells
551 .iter()
552 .filter(|cell| cell.is_vmerge_continue)
553 .map(|cell| (cell.col_index, cell.grid_span))
554 .collect::<Vec<_>>()
555 })
556 .unwrap_or_default();
557 for cell in &mut rows[row_index].cells {
558 cell.height = row_heights[row_index];
559 cell.merged_height = row_heights[row_index];
560 cell.merge_with_below = continuing_spans.contains(&(cell.col_index, cell.grid_span));
561 }
562 }
563 for (row_index, cell_index, last_row, required) in spans {
564 let restart = &mut rows[row_index].cells[cell_index];
565 restart.merged_height = row_heights[row_index..=last_row].iter().sum();
566 restart.is_last_row = last_row + 1 == num_rows;
567 restart.clip_content = required > restart.merged_height;
568 }
569
570 Ok((
571 TableBlock {
572 structure_id: None,
573 col_widths,
574 rows,
575 header_row_indices,
576 table_width,
577 table_indent,
578 borders: table_borders,
579 },
580 TableSemantics {
581 rows: row_semantics,
582 },
583 ))
584}
585
586fn compute_column_widths(
593 grid: Option<&CT_TblGrid>,
594 available_width: f64,
595 table: &CT_Tbl,
596 path: &[usize],
597) -> Vec<f64> {
598 match grid {
599 Some(g) if !g.columns.is_empty() => {
600 let widths: Vec<f64> = g.columns.iter().map(|c| c.width.to_pt()).collect();
601 let total: f64 = widths.iter().sum();
602 if total < 0.01 {
603 let n = g.columns.len();
605 vec![available_width / n as f64; n]
606 } else if total > available_width + 1.0 {
607 let scale = available_width / total;
609 widths.iter().map(|w| w * scale).collect()
610 } else {
611 widths
612 }
613 }
614 _ => {
615 let num_cols = layout_table_rows(table, path)
617 .first()
618 .map(|(row, path)| {
619 layout_row_cells(row, path)
620 .iter()
621 .map(|(cell, _)| {
622 cell.properties
623 .as_ref()
624 .and_then(|p| p.grid_span)
625 .unwrap_or(1) as usize
626 })
627 .sum::<usize>()
628 })
629 .unwrap_or(1)
630 .max(1);
631 vec![available_width / num_cols as f64; num_cols]
632 }
633 }
634}
635
636fn layout_cell_content(
640 content: &[rdocx_oxml::table::CellContent],
641 available_width: f64,
642 styles: &CT_Styles,
643 input: &LayoutInput,
644 media: &MediaRegistry,
645 fm: &mut FontManager,
646 num_state: &mut NumberingState,
647 diagnostics: &mut Vec<Diagnostic>,
648 sources: Option<&SourceRegistry>,
649 story: &WordStory,
650 cell_path: &[usize],
651 table_style_ppr: Option<&rdocx_oxml::properties::CT_PPr>,
652) -> Result<(Vec<CellBlock>, Vec<CellBlockSemantics>)> {
653 use crate::engine;
654 use rdocx_oxml::table::CellContent;
655
656 let mut blocks = Vec::new();
657 let mut semantics = Vec::new();
658 for (content_index, item) in content.iter().enumerate() {
659 let mut source_path = cell_path.to_vec();
660 source_path.push(content_index);
661 match item {
662 CellContent::Paragraph(para) => {
663 let source = sources.and_then(|sources| sources.id(story, &source_path));
664 let (block, reflow_direction) = engine::layout_paragraph_with_source_in_table(
665 para,
666 available_width,
667 styles,
668 input,
669 media,
670 fm,
671 num_state,
672 diagnostics,
673 source,
674 table_style_ppr,
675 )?;
676 blocks.push(CellBlock::Paragraph(block));
677 semantics.push(CellBlockSemantics::Paragraph(ParagraphSemantics {
678 source_node: source,
679 structure_id: None,
680 reflow_direction,
681 }));
682 }
683 CellContent::Table(tbl) => {
684 let (nested, nested_semantics) = layout_table_inner(
686 tbl,
687 available_width,
688 styles,
689 input,
690 media,
691 fm,
692 num_state,
693 diagnostics,
694 sources,
695 story,
696 &source_path,
697 )?;
698 blocks.push(CellBlock::Table(nested));
699 semantics.push(CellBlockSemantics::Table(nested_semantics));
700 }
701 CellContent::ContentControl(control) => layout_control_cell_content(
702 control,
703 available_width,
704 styles,
705 input,
706 media,
707 fm,
708 num_state,
709 diagnostics,
710 sources,
711 story,
712 &source_path,
713 table_style_ppr,
714 &mut blocks,
715 &mut semantics,
716 )?,
717 }
718 }
719 Ok((blocks, semantics))
720}
721
722#[allow(clippy::too_many_arguments)]
723fn layout_control_cell_content(
724 control: &CT_Sdt,
725 available_width: f64,
726 styles: &CT_Styles,
727 input: &LayoutInput,
728 media: &MediaRegistry,
729 fm: &mut FontManager,
730 num_state: &mut NumberingState,
731 diagnostics: &mut Vec<Diagnostic>,
732 sources: Option<&SourceRegistry>,
733 story: &WordStory,
734 path: &[usize],
735 table_style_ppr: Option<&rdocx_oxml::properties::CT_PPr>,
736 blocks: &mut Vec<CellBlock>,
737 semantics: &mut Vec<CellBlockSemantics>,
738) -> Result<()> {
739 use crate::engine;
740
741 for (content_index, content) in control.content.iter().enumerate() {
742 let mut source_path = path.to_vec();
743 source_path.push(content_index);
744 match content {
745 SdtContent::Paragraph(paragraph) => {
746 let source = sources.and_then(|sources| sources.id(story, &source_path));
747 let (block, reflow_direction) = engine::layout_paragraph_with_source_in_table(
748 paragraph,
749 available_width,
750 styles,
751 input,
752 media,
753 fm,
754 num_state,
755 diagnostics,
756 source,
757 table_style_ppr,
758 )?;
759 blocks.push(CellBlock::Paragraph(block));
760 semantics.push(CellBlockSemantics::Paragraph(ParagraphSemantics {
761 source_node: source,
762 structure_id: None,
763 reflow_direction,
764 }));
765 }
766 SdtContent::Table(table) => {
767 let (nested, nested_semantics) = layout_table_inner(
768 table,
769 available_width,
770 styles,
771 input,
772 media,
773 fm,
774 num_state,
775 diagnostics,
776 sources,
777 story,
778 &source_path,
779 )?;
780 blocks.push(CellBlock::Table(nested));
781 semantics.push(CellBlockSemantics::Table(nested_semantics));
782 }
783 SdtContent::ContentControl(control) => layout_control_cell_content(
784 control,
785 available_width,
786 styles,
787 input,
788 media,
789 fm,
790 num_state,
791 diagnostics,
792 sources,
793 story,
794 &source_path,
795 table_style_ppr,
796 blocks,
797 semantics,
798 )?,
799 SdtContent::Row(_)
800 | SdtContent::Cell(_)
801 | SdtContent::Run(_)
802 | SdtContent::RawXml(_) => {}
803 }
804 }
805 Ok(())
806}
807
808#[derive(Default)]
809struct ResolvedTableCellStyle {
810 paragraph_properties: Option<rdocx_oxml::properties::CT_PPr>,
811 borders: Option<CT_TblBorders>,
812 shading: Option<rdocx_oxml::properties::CT_Shd>,
813}
814
815fn resolve_table_style_cell(
816 table: &CT_Tbl,
817 styles: &CT_Styles,
818 row: usize,
819 column: usize,
820 row_count: usize,
821 column_count: usize,
822 row_cnf_style: Option<&str>,
823 cell_cnf_style: Option<&str>,
824) -> ResolvedTableCellStyle {
825 let Some(mut style_id) = table
826 .properties
827 .as_ref()
828 .and_then(|p| p.style_id.as_deref())
829 else {
830 return ResolvedTableCellStyle::default();
831 };
832 let mut chain = Vec::new();
833 let mut visited = std::collections::HashSet::new();
834 while visited.insert(style_id) {
835 let Some(style) = styles.get_by_id(style_id) else {
836 break;
837 };
838 chain.push(style);
839 let Some(base) = style.based_on.as_deref() else {
840 break;
841 };
842 style_id = base;
843 }
844 let mut resolved = ResolvedTableCellStyle::default();
845 for style in chain.into_iter().rev() {
846 if let Some(properties) = &style.ppr {
847 resolved
848 .paragraph_properties
849 .get_or_insert_with(rdocx_oxml::properties::CT_PPr::default)
850 .merge_from(properties);
851 }
852 if let Some(borders) = style
853 .table_properties
854 .as_ref()
855 .and_then(|properties| properties.borders.as_ref())
856 {
857 overlay_borders(&mut resolved.borders, borders);
858 }
859 if let Some(shading) = style
860 .table_properties
861 .as_ref()
862 .and_then(|properties| properties.shading.as_ref())
863 {
864 resolved.shading = Some(shading.clone());
865 }
866 for region in applicable_table_regions(
867 table,
868 row,
869 column,
870 row_count,
871 column_count,
872 row_cnf_style,
873 cell_cnf_style,
874 ) {
875 for conditional in style
876 .conditional_table_styles
877 .iter()
878 .filter(|conditional| conditional.region == region)
879 {
880 if let Some(properties) = &conditional.paragraph_properties {
881 resolved
882 .paragraph_properties
883 .get_or_insert_with(rdocx_oxml::properties::CT_PPr::default)
884 .merge_from(properties);
885 }
886 if let Some(borders) = conditional
887 .cell_properties
888 .as_ref()
889 .and_then(|properties| properties.borders.as_ref())
890 .or_else(|| {
891 conditional
892 .table_properties
893 .as_ref()
894 .and_then(|properties| properties.borders.as_ref())
895 })
896 {
897 overlay_borders(&mut resolved.borders, borders);
898 }
899 if let Some(shading) = conditional
900 .cell_properties
901 .as_ref()
902 .and_then(|properties| properties.shading.as_ref())
903 .or_else(|| {
904 conditional
905 .table_properties
906 .as_ref()
907 .and_then(|properties| properties.shading.as_ref())
908 })
909 {
910 resolved.shading = Some(shading.clone());
911 }
912 }
913 }
914 }
915 resolved
916}
917
918fn applicable_table_regions(
919 table: &CT_Tbl,
920 row: usize,
921 column: usize,
922 row_count: usize,
923 column_count: usize,
924 row_cnf_style: Option<&str>,
925 cell_cnf_style: Option<&str>,
926) -> Vec<&'static str> {
927 let cnf = |index: usize| {
928 [row_cnf_style, cell_cnf_style]
929 .into_iter()
930 .flatten()
931 .any(|value| value.as_bytes().get(index) == Some(&b'1'))
932 };
933 let look = table
934 .properties
935 .as_ref()
936 .and_then(|properties| properties.look.as_ref());
937 let enabled = |explicit: Option<bool>, mask: u16, default: bool| {
938 explicit.unwrap_or_else(|| {
939 look.and_then(|look| look.val.as_deref())
940 .and_then(|value| u16::from_str_radix(value, 16).ok())
941 .map_or(default, |value| value & mask != 0)
942 })
943 };
944 let first_row =
945 (enabled(look.and_then(|look| look.first_row), 0x20, false) && row == 0) || cnf(0);
946 let last_row = (enabled(look.and_then(|look| look.last_row), 0x40, false)
947 && row + 1 == row_count)
948 || cnf(1);
949 let first_column =
950 (enabled(look.and_then(|look| look.first_column), 0x80, false) && column == 0) || cnf(2);
951 let last_column = (enabled(look.and_then(|look| look.last_column), 0x100, false)
952 && column + 1 == column_count)
953 || cnf(3);
954 let no_h_band = enabled(look.and_then(|look| look.no_h_band), 0x200, false);
955 let no_v_band = enabled(look.and_then(|look| look.no_v_band), 0x400, false);
956
957 let mut regions = vec!["wholeTable"];
958 if cnf(6) {
959 regions.push("band1Horz");
960 } else if cnf(7) {
961 regions.push("band2Horz");
962 } else if !no_h_band {
963 regions.push(if row.is_multiple_of(2) {
964 "band1Horz"
965 } else {
966 "band2Horz"
967 });
968 }
969 if cnf(4) {
970 regions.push("band1Vert");
971 } else if cnf(5) {
972 regions.push("band2Vert");
973 } else if !no_v_band {
974 regions.push(if column.is_multiple_of(2) {
975 "band1Vert"
976 } else {
977 "band2Vert"
978 });
979 }
980 if first_column {
981 regions.push("firstCol");
982 }
983 if last_column {
984 regions.push("lastCol");
985 }
986 if first_row {
987 regions.push("firstRow");
988 }
989 if last_row {
990 regions.push("lastRow");
991 }
992 if cnf(9) {
993 regions.push("nwCell");
994 } else if cnf(8) {
995 regions.push("neCell");
996 } else if cnf(11) {
997 regions.push("swCell");
998 } else if cnf(10) {
999 regions.push("seCell");
1000 } else {
1001 match (first_row, last_row, first_column, last_column) {
1002 (true, _, true, _) => regions.push("nwCell"),
1003 (true, _, _, true) => regions.push("neCell"),
1004 (_, true, true, _) => regions.push("swCell"),
1005 (_, true, _, true) => regions.push("seCell"),
1006 _ => {}
1007 }
1008 }
1009 regions
1010}
1011
1012fn overlay_borders(target: &mut Option<CT_TblBorders>, source: &CT_TblBorders) {
1013 let target = target.get_or_insert_with(CT_TblBorders::default);
1014 if source.top.is_some() {
1015 target.top = source.top.clone();
1016 }
1017 if source.bottom.is_some() {
1018 target.bottom = source.bottom.clone();
1019 }
1020 if source.left.is_some() {
1021 target.left = source.left.clone();
1022 }
1023 if source.right.is_some() {
1024 target.right = source.right.clone();
1025 }
1026 if source.inside_h.is_some() {
1027 target.inside_h = source.inside_h.clone();
1028 }
1029 if source.inside_v.is_some() {
1030 target.inside_v = source.inside_v.clone();
1031 }
1032}
1033
1034#[cfg(test)]
1035mod tests {
1036 use super::*;
1037 use rdocx_oxml::table::{
1038 CT_Row, CT_TblGrid, CT_TblGridCol, CT_TblLook, CT_TblPr, CT_Tc, CT_TcPr, CT_TrPr,
1039 };
1040 use rdocx_oxml::units::Twips;
1041
1042 fn layout_with_defaults(table: &CT_Tbl, width: f64) -> TableBlock {
1043 let styles = CT_Styles::default();
1044 layout_with_styles(table, width, &styles)
1045 }
1046
1047 fn layout_with_styles(table: &CT_Tbl, width: f64, styles: &CT_Styles) -> TableBlock {
1048 let input = LayoutInput {
1049 revision_view: crate::input::RevisionView::Accepted,
1050 automatic_hyphenation: false,
1051 math_properties: None,
1052 document: rdocx_oxml::document::CT_Document {
1053 body: rdocx_oxml::document::CT_Body {
1054 content: Vec::new(),
1055 sect_pr: None,
1056 },
1057 extra_namespaces: Vec::new(),
1058 background_xml: None,
1059 background_extra_xml: Vec::new(),
1060 },
1061 styles: styles.clone(),
1062 numbering: None,
1063 headers: std::collections::HashMap::new(),
1064 footers: std::collections::HashMap::new(),
1065 images: std::collections::HashMap::new(),
1066 charts: std::collections::HashMap::new(),
1067 chart_theme: oxml_drawing::theme::CT_OfficeStyleSheet::office_default(),
1068 chart_color_map: oxml_drawing::color::ColorMap::default(),
1069 hyperlink_urls: std::collections::HashMap::new(),
1070 footnotes: None,
1071 endnotes: None,
1072 core_properties: None,
1073 theme: None,
1074 fonts: Vec::new(),
1075 };
1076 let media = MediaRegistry::new(&input.images);
1077 let mut font_manager = FontManager::new();
1078 let mut numbering = NumberingState::new();
1079 layout_table(
1080 table,
1081 width,
1082 styles,
1083 &input,
1084 &media,
1085 &mut font_manager,
1086 &mut numbering,
1087 &mut Vec::new(),
1088 )
1089 .unwrap()
1090 }
1091
1092 #[test]
1093 fn narrow_grid_keeps_its_declared_width() {
1094 let tbl = CT_Tbl::new();
1095 let grid = CT_TblGrid {
1096 columns: vec![
1097 CT_TblGridCol { width: Twips(2880) }, CT_TblGridCol { width: Twips(2880) },
1099 ],
1100 ..Default::default()
1101 };
1102
1103 let widths = compute_column_widths(Some(&grid), 468.0, &tbl, &[]);
1106
1107 assert_eq!(widths.len(), 2);
1108 let total: f64 = widths.iter().sum();
1109 assert!((total - 288.0).abs() < 1.0, "got {total}");
1110 }
1111
1112 #[test]
1113 fn historical_table_grid_never_changes_active_column_widths() {
1114 let table = CT_Tbl::new();
1115 let grid = CT_TblGrid {
1116 columns: vec![
1117 CT_TblGridCol { width: Twips(1440) },
1118 CT_TblGridCol { width: Twips(2880) },
1119 ],
1120 grid_change_xml: Some(
1121 br#"<w:tblGridChange w:id="4"><w:tblGrid><w:gridCol w:w="9000"/><w:gridCol w:w="9000"/></w:tblGrid></w:tblGridChange>"#
1122 .to_vec(),
1123 ),
1124 ..CT_TblGrid::default()
1125 };
1126
1127 assert_eq!(
1128 compute_column_widths(Some(&grid), 468.0, &table, &[]),
1129 vec![72.0, 144.0]
1130 );
1131 }
1132
1133 #[test]
1134 fn overflowing_grid_is_scaled_down_to_fit() {
1135 let tbl = CT_Tbl::new();
1136 let grid = CT_TblGrid {
1137 columns: vec![
1138 CT_TblGridCol { width: Twips(7200) }, CT_TblGridCol { width: Twips(7200) },
1140 ],
1141 ..Default::default()
1142 };
1143
1144 let widths = compute_column_widths(Some(&grid), 468.0, &tbl, &[]);
1146
1147 let total: f64 = widths.iter().sum();
1148 assert!((total - 468.0).abs() < 1.0, "got {total}");
1149 assert!((widths[0] - widths[1]).abs() < 0.01);
1151 }
1152
1153 #[test]
1154 fn column_widths_no_grid() {
1155 let tbl = CT_Tbl::new();
1156 let widths = compute_column_widths(None, 468.0, &tbl, &[]);
1157 assert_eq!(widths.len(), 1);
1158 assert!((widths[0] - 468.0).abs() < 0.01);
1159 }
1160
1161 #[test]
1162 fn column_widths_zero_grid() {
1163 let tbl = CT_Tbl::new();
1164 let grid = CT_TblGrid {
1165 columns: vec![
1166 CT_TblGridCol { width: Twips(0) },
1167 CT_TblGridCol { width: Twips(0) },
1168 CT_TblGridCol { width: Twips(0) },
1169 ],
1170 ..Default::default()
1171 };
1172 let widths = compute_column_widths(Some(&grid), 468.0, &tbl, &[]);
1173 assert_eq!(widths.len(), 3);
1174 for w in &widths {
1175 assert!((w - 156.0).abs() < 0.01);
1176 }
1177 }
1178
1179 #[test]
1180 fn column_widths_inferred_from_rows() {
1181 use rdocx_oxml::table::{CT_Row, CT_Tc};
1182 let mut tbl = CT_Tbl::new();
1183 let mut row = CT_Row::new();
1184 row.cells.push(CT_Tc::new());
1185 row.cells.push(CT_Tc::new());
1186 row.cells.push(CT_Tc::new());
1187 tbl.rows.push(row);
1188 let widths = compute_column_widths(None, 300.0, &tbl, &[]);
1189 assert_eq!(widths.len(), 3);
1190 for w in &widths {
1191 assert!((w - 100.0).abs() < 0.01);
1192 }
1193 }
1194
1195 #[test]
1196 fn nested_tables_remain_recursive_cell_blocks() {
1197 use rdocx_oxml::table::{CT_Row, CT_Tbl, CT_Tc, CellContent};
1198
1199 let mut outer = CT_Tbl::new();
1201 outer.grid = Some(CT_TblGrid {
1202 columns: vec![CT_TblGridCol { width: Twips(4680) }], ..Default::default()
1204 });
1205
1206 let mut outer_row = CT_Row::new();
1207 let mut outer_cell = CT_Tc::new();
1208 outer_cell.paragraphs_mut()[0].add_run("Before nested");
1209
1210 let mut nested = CT_Tbl::new();
1212 nested.grid = Some(CT_TblGrid {
1213 columns: vec![
1214 CT_TblGridCol { width: Twips(2000) },
1215 CT_TblGridCol { width: Twips(2000) },
1216 ],
1217 ..Default::default()
1218 });
1219 let mut nr = CT_Row::new();
1220 let mut nc1 = CT_Tc::new();
1221 nc1.paragraphs_mut()[0].add_run("N1");
1222 let mut nc2 = CT_Tc::new();
1223 nc2.paragraphs_mut()[0].add_run("N2");
1224 nr.cells.push(nc1);
1225 nr.cells.push(nc2);
1226 nested.rows.push(nr);
1227
1228 outer_cell.content.push(CellContent::Table(nested));
1229 outer_row.cells.push(outer_cell);
1230 outer.rows.push(outer_row);
1231
1232 let styles = rdocx_oxml::styles::CT_Styles::default();
1234 let input = crate::input::LayoutInput {
1235 revision_view: crate::input::RevisionView::Accepted,
1236 automatic_hyphenation: false,
1237 math_properties: None,
1238 document: rdocx_oxml::document::CT_Document {
1239 body: rdocx_oxml::document::CT_Body {
1240 content: Vec::new(),
1241 sect_pr: None,
1242 },
1243 extra_namespaces: Vec::new(),
1244 background_xml: None,
1245 background_extra_xml: Vec::new(),
1246 },
1247 styles: styles.clone(),
1248 numbering: None,
1249 headers: std::collections::HashMap::new(),
1250 footers: std::collections::HashMap::new(),
1251 images: std::collections::HashMap::new(),
1252 charts: std::collections::HashMap::new(),
1253 chart_theme: oxml_drawing::theme::CT_OfficeStyleSheet::office_default(),
1254 chart_color_map: oxml_drawing::color::ColorMap::default(),
1255 hyperlink_urls: std::collections::HashMap::new(),
1256 footnotes: None,
1257 endnotes: None,
1258 core_properties: None,
1259 theme: None,
1260 fonts: Vec::new(),
1261 };
1262
1263 let mut fm = FontManager::new();
1264 let mut num_state = crate::style_resolver::NumberingState::new();
1265 let mut diagnostics = Vec::new();
1266 let media = MediaRegistry::new(&input.images);
1267
1268 let result = layout_table(
1269 &outer,
1270 234.0,
1271 &styles,
1272 &input,
1273 &media,
1274 &mut fm,
1275 &mut num_state,
1276 &mut diagnostics,
1277 );
1278 assert!(result.is_ok());
1279 let block = result.unwrap();
1280
1281 assert_eq!(block.rows.len(), 1);
1283 assert_eq!(block.rows[0].cells.len(), 1);
1284
1285 let cell = &block.rows[0].cells[0];
1287 assert_eq!(cell.blocks.len(), 2);
1288 assert!(matches!(cell.blocks[0], CellBlock::Paragraph(_)));
1289 assert!(matches!(cell.blocks[1], CellBlock::Table(_)));
1290
1291 assert!((block.table_width - 234.0).abs() < 1.0);
1293 }
1294
1295 #[test]
1296 fn vertical_merges_and_row_height_rules_share_the_exact_grid_span() {
1297 let mut table = CT_Tbl::new();
1298 table.grid = Some(CT_TblGrid {
1299 columns: vec![
1300 CT_TblGridCol { width: Twips(600) },
1301 CT_TblGridCol { width: Twips(600) },
1302 ],
1303 ..Default::default()
1304 });
1305
1306 let mut exact_row = CT_Row::new();
1307 exact_row.properties = Some(CT_TrPr {
1308 height: Some(Twips(200)),
1309 height_rule: Some("exact".to_owned()),
1310 ..Default::default()
1311 });
1312 let mut restart = CT_Tc::new();
1313 restart.properties = Some(CT_TcPr {
1314 grid_span: Some(2),
1315 v_merge: Some(VMerge::Restart),
1316 ..Default::default()
1317 });
1318 restart.paragraphs_mut()[0].add_run(
1319 "merged content wraps across enough words to require both rows and grow only a minimum row",
1320 );
1321 exact_row.cells.push(restart);
1322
1323 let mut minimum_row = CT_Row::new();
1324 minimum_row.properties = Some(CT_TrPr {
1325 height: Some(Twips(200)),
1326 height_rule: Some("atLeast".to_owned()),
1327 ..Default::default()
1328 });
1329 let mut continuation = CT_Tc::new();
1330 continuation.properties = Some(CT_TcPr {
1331 grid_span: Some(2),
1332 v_merge: Some(VMerge::Continue),
1333 ..Default::default()
1334 });
1335 minimum_row.cells.push(continuation);
1336 table.rows = vec![exact_row, minimum_row];
1337
1338 let block = layout_with_defaults(&table, 60.0);
1339 assert_eq!(block.rows[0].height, 10.0, "exact row must stay pinned");
1340 assert!(block.rows[1].height >= 10.0);
1341 let restart = &block.rows[0].cells[0];
1342 assert_eq!(restart.grid_span, 2);
1343 assert!(restart.merge_with_below);
1344 assert_eq!(
1345 restart.merged_height,
1346 block.rows[0].height + block.rows[1].height
1347 );
1348 assert!(
1349 restart.is_last_row,
1350 "merge ends on the table's outer bottom"
1351 );
1352 assert!(block.rows[1].cells[0].is_vmerge_continue);
1353
1354 let mut minimum_merge = CT_Tbl::new();
1355 minimum_merge.grid = Some(CT_TblGrid {
1356 columns: vec![CT_TblGridCol { width: Twips(600) }],
1357 ..Default::default()
1358 });
1359 let mut restart_row = CT_Row::new();
1360 let mut restart = CT_Tc::new();
1361 restart.properties = Some(CT_TcPr {
1362 v_merge: Some(VMerge::Restart),
1363 ..Default::default()
1364 });
1365 restart.paragraphs_mut()[0]
1366 .add_run("merged content grows the final eligible row in this span");
1367 restart_row.cells.push(restart);
1368 let mut final_row = CT_Row::new();
1369 final_row.properties = Some(CT_TrPr {
1370 height: Some(Twips(200)),
1371 height_rule: Some("atLeast".to_owned()),
1372 ..Default::default()
1373 });
1374 let mut continuation = CT_Tc::new();
1375 continuation.properties = Some(CT_TcPr {
1376 v_merge: Some(VMerge::Continue),
1377 ..Default::default()
1378 });
1379 final_row.cells.push(continuation);
1380 minimum_merge.rows = vec![restart_row, final_row];
1381
1382 let minimum_block = layout_with_defaults(&minimum_merge, 30.0);
1383 assert_eq!(minimum_block.rows[0].height, 0.0);
1384 assert!(
1385 minimum_block.rows[1].height > 10.0,
1386 "restart content grows the final non-exact row"
1387 );
1388 }
1389
1390 #[test]
1391 fn table_style_cascade_resolves_borders_and_paragraph_spacing() {
1392 let styles = CT_Styles::from_xml(
1393 format!(
1394 r#"<w:styles xmlns:w="{}"><w:style w:type="table" w:styleId="Base"><w:pPr><w:spacing w:after="80"/></w:pPr><w:tblPr><w:tblBorders><w:left w:val="single" w:sz="8" w:color="AA0000"/></w:tblBorders></w:tblPr></w:style><w:style w:type="table" w:styleId="Dense"><w:basedOn w:val="Base"/><w:pPr><w:spacing w:after="40"/></w:pPr><w:tblStylePr w:type="firstRow"><w:pPr><w:spacing w:after="0"/></w:pPr><w:tcPr><w:tcBorders><w:top w:val="double" w:sz="12" w:color="0000AA"/></w:tcBorders><w:shd w:val="clear" w:fill="DDEEFF"/></w:tcPr></w:tblStylePr><w:tblStylePr w:type="firstCol"><w:tcPr><w:shd w:val="clear" w:fill="CCFFCC"/></w:tcPr></w:tblStylePr></w:style></w:styles>"#,
1395 rdocx_oxml::namespace::W_NS
1396 )
1397 .as_bytes(),
1398 )
1399 .unwrap();
1400 let mut table = CT_Tbl::new();
1401 table.properties = Some(CT_TblPr {
1402 style_id: Some("Dense".to_owned()),
1403 look: Some(CT_TblLook {
1404 first_row: Some(false),
1405 first_column: Some(false),
1406 no_h_band: Some(true),
1407 no_v_band: Some(true),
1408 ..Default::default()
1409 }),
1410 ..Default::default()
1411 });
1412 table.grid = Some(CT_TblGrid {
1413 columns: vec![CT_TblGridCol { width: Twips(1200) }],
1414 ..Default::default()
1415 });
1416 for (index, text) in ["header", "body"].into_iter().enumerate() {
1417 let mut row = CT_Row::new();
1418 let mut cell = CT_Tc::new();
1419 if index == 0 {
1420 row.properties = Some(CT_TrPr {
1421 cnf_style: Some("100000000000".to_owned()),
1422 ..Default::default()
1423 });
1424 } else {
1425 cell.properties = Some(CT_TcPr {
1426 cnf_style: Some("001000000000".to_owned()),
1427 ..Default::default()
1428 });
1429 }
1430 cell.paragraphs_mut()[0].add_run(text);
1431 row.cells.push(cell);
1432 table.rows.push(row);
1433 }
1434
1435 let block = layout_with_styles(&table, 60.0, &styles);
1436 let CellBlock::Paragraph(header) = &block.rows[0].cells[0].blocks[0] else {
1437 panic!("header paragraph");
1438 };
1439 let CellBlock::Paragraph(body) = &block.rows[1].cells[0].blocks[0] else {
1440 panic!("body paragraph");
1441 };
1442 assert_eq!(header.space_after, 0.0);
1443 assert_eq!(body.space_after, 2.0);
1444 assert_eq!(
1445 block.rows[0].cells[0].shading,
1446 Some(Color::from_hex("DDEEFF"))
1447 );
1448 assert_eq!(
1449 block.rows[1].cells[0].shading,
1450 Some(Color::from_hex("CCFFCC"))
1451 );
1452 let header_borders = block.rows[0].cells[0].borders.as_ref().unwrap();
1453 assert_eq!(header_borders.top.as_ref().unwrap().sz, Some(12));
1454 assert_eq!(
1455 header_borders.top.as_ref().unwrap().color.as_deref(),
1456 Some("0000AA")
1457 );
1458 assert_eq!(
1459 header_borders.left.as_ref().unwrap().color.as_deref(),
1460 Some("AA0000")
1461 );
1462 }
1463}