Skip to main content

rust_docx/parse/
table.rs

1use std::collections::HashMap;
2
3use crate::model::{
4    ParagraphAlignment, TableBorderSet, TableBoxSpacing, TableCellContentNode, TableCellNode,
5    TableCellNodeType, TableCellStyle, TableCellVerticalAlign, TableLayout, TableNode,
6    TableNodeType, TableRowGeometrySource, TableRowHeightRule, TableRowNode, TableRowNodeType,
7    TableRowStyle, TableStyle, TableWidthSource, TextStyle,
8};
9use crate::parse::context::{
10    default_table_look, ParsedTableLook, ParsedTableStyleCondition, ParsedTableStyleDefinition,
11    TableConditionalStyleType,
12};
13use crate::parse::paragraph::parse_paragraph_with_table_run_style;
14use crate::parse::re;
15use crate::parse::styles::{
16    direct_table_property, merge_table_style_properties, parse_table_box_spacing,
17    parse_table_preferred_width, parse_table_style_properties_from_xml, preferred_width_twips,
18};
19use crate::parse::util::{
20    merge_table_border_sets, merge_text_styles, normalize_hex_color, parse_table_border_set,
21};
22use crate::xml::{
23    extract_balanced_tag_blocks, extract_balanced_tag_blocks_in_order, get_attribute,
24    parse_integer_attribute, parse_on_off_attribute,
25};
26
27pub fn parse_table_cell_content(
28    cell_xml: &str,
29    context: &crate::parse::context::ParseContext<
30        '_,
31        impl std::borrow::Borrow<super::context::ParsedStyleSheet>,
32    >,
33    table_paragraph_spacing: Option<&crate::model::ParagraphSpacing>,
34) -> Vec<TableCellContentNode> {
35    parse_table_cell_content_with_run_style(cell_xml, context, table_paragraph_spacing, None)
36}
37
38fn parse_table_cell_content_with_run_style(
39    cell_xml: &str,
40    context: &crate::parse::context::ParseContext<
41        '_,
42        impl std::borrow::Borrow<super::context::ParsedStyleSheet>,
43    >,
44    table_paragraph_spacing: Option<&crate::model::ParagraphSpacing>,
45    table_run_style: Option<&TextStyle>,
46) -> Vec<TableCellContentNode> {
47    let block_ranges = extract_balanced_tag_blocks_in_order(cell_xml, &["w:p", "w:tbl"]);
48    let parsed: Vec<TableCellContentNode> = block_ranges
49        .iter()
50        .filter_map(|block| {
51            let block_xml = &cell_xml[block.start..block.end];
52            if block_xml.starts_with("<w:p") || block_xml.starts_with("<W:p") {
53                return Some(TableCellContentNode::Paragraph(
54                    parse_paragraph_with_table_run_style(
55                        block_xml,
56                        context,
57                        table_paragraph_spacing,
58                        table_run_style,
59                    ),
60                ));
61            }
62            if block_xml.starts_with("<w:tbl") || block_xml.starts_with("<W:tbl") {
63                return Some(TableCellContentNode::Table(Box::new(parse_table(
64                    block_xml, context,
65                ))));
66            }
67            None
68        })
69        .collect();
70    if !parsed.is_empty() {
71        return parsed;
72    }
73    vec![TableCellContentNode::Paragraph(
74        parse_paragraph_with_table_run_style(
75            "<w:p><w:r><w:t/></w:r></w:p>",
76            context,
77            table_paragraph_spacing,
78            table_run_style,
79        ),
80    )]
81}
82
83#[derive(Clone, Debug)]
84pub struct ParsedTableCellResult {
85    pub cell: TableCellNode,
86    pub v_merge: Option<String>,
87}
88
89pub fn parse_table_cell(
90    cell_xml: &str,
91    context: &crate::parse::context::ParseContext<
92        '_,
93        impl std::borrow::Borrow<super::context::ParsedStyleSheet>,
94    >,
95    table_paragraph_spacing: Option<&crate::model::ParagraphSpacing>,
96) -> ParsedTableCellResult {
97    let mut result = parse_table_cell_geometry(cell_xml);
98    result.cell.nodes = parse_table_cell_content(cell_xml, context, table_paragraph_spacing);
99    result
100}
101
102#[derive(Default)]
103struct CellPropertyTokens<'a> {
104    values: [Option<&'a str>; 9],
105    margins: [Option<&'a str>; 6],
106    borders: [Option<&'a str>; 8],
107}
108
109fn indexed_cell_property_tokens(xml: &str) -> Option<CellPropertyTokens<'_>> {
110    let mut tokens = CellPropertyTokens::default();
111    let mut container = None;
112    let complete = crate::xml_index::for_each_element(xml, |element| {
113        if element.source_start == 0 {
114            return element.name == "w:tcPr";
115        }
116        let leaf = element.content().is_none_or(|text| text.trim().is_empty());
117        if let Some((kind, end)) = container {
118            if element.source_start < end {
119                if !leaf {
120                    return false;
121                }
122                let edge = match element.name {
123                    "w:top" => 0,
124                    "w:right" => 1,
125                    "w:bottom" => 2,
126                    "w:left" => 3,
127                    "w:start" if kind == 3 => 4,
128                    "w:end" if kind == 3 => 5,
129                    "w:insideH" if kind == 4 => 4,
130                    "w:insideV" if kind == 4 => 5,
131                    "w:tl2br" if kind == 4 => 6,
132                    "w:tr2bl" if kind == 4 => 7,
133                    _ => return false,
134                };
135                let slots = if kind == 3 {
136                    tokens.margins.as_mut_slice()
137                } else {
138                    tokens.borders.as_mut_slice()
139                };
140                slots[edge].get_or_insert(element.xml);
141                return true;
142            }
143            container = None;
144        }
145        let property = match element.name {
146            "w:shd" => 0,
147            "w:gridSpan" => 1,
148            "w:tcW" => 2,
149            "w:tcMar" => 3,
150            "w:tcBorders" => 4,
151            "w:vAlign" => 5,
152            "w:vMerge" => 6,
153            "w:textDirection" => 7,
154            "w:noWrap" => 8,
155            "w:cnfStyle" | "w:tcFitText" | "w:hideMark" | "w:hMerge" | "w:cellIns"
156            | "w:cellDel" | "w:cellMerge" => return leaf,
157            _ => return false,
158        };
159        if tokens.values[property].is_some() {
160            return false;
161        }
162        if matches!(property, 3 | 4) {
163            container = Some((property, element.source_start + element.xml.len()));
164        } else if !leaf {
165            return false;
166        }
167        tokens.values[property] = Some(element.xml);
168        true
169    })?;
170    complete.then_some(tokens)
171}
172
173impl CellPropertyTokens<'_> {
174    fn margin_spacing(&self) -> Option<TableBoxSpacing> {
175        let spacing = TableBoxSpacing {
176            top_twips: self.margins[0].and_then(|tag| parse_integer_attribute(tag, "w:w")),
177            right_twips: self.margins[1]
178                .or(self.margins[5])
179                .and_then(|tag| parse_integer_attribute(tag, "w:w")),
180            bottom_twips: self.margins[2].and_then(|tag| parse_integer_attribute(tag, "w:w")),
181            left_twips: self.margins[3]
182                .or(self.margins[4])
183                .and_then(|tag| parse_integer_attribute(tag, "w:w")),
184        };
185        (spacing.top_twips.is_some()
186            || spacing.right_twips.is_some()
187            || spacing.bottom_twips.is_some()
188            || spacing.left_twips.is_some())
189        .then_some(spacing)
190    }
191
192    fn border_set(&self) -> Option<TableBorderSet> {
193        let [top, right, bottom, left, inside_h, inside_v, tl2br, tr2bl] =
194            self.borders.map(super::util::parse_table_border_style);
195        let present = top.is_some()
196            || right.is_some()
197            || bottom.is_some()
198            || left.is_some()
199            || inside_h.is_some()
200            || inside_v.is_some()
201            || tl2br.is_some()
202            || tr2bl.is_some();
203        present.then_some(TableBorderSet {
204            top,
205            right,
206            bottom,
207            left,
208            inside_h,
209            inside_v,
210            tl2br,
211            tr2bl,
212        })
213    }
214}
215
216fn parse_table_cell_geometry(cell_xml: &str) -> ParsedTableCellResult {
217    parse_table_cell_properties(direct_table_property(cell_xml, "w:tcPr"))
218}
219
220fn parse_table_cell_properties(source: Option<&str>) -> ParsedTableCellResult {
221    let tokens = source.and_then(indexed_cell_property_tokens);
222    let owned_fallback = if tokens.is_none() {
223        source.map(str::to_owned)
224    } else {
225        None
226    };
227    let cell_properties_xml = owned_fallback.as_deref();
228    let property = |index, name| {
229        if let Some(tokens) = &tokens {
230            tokens.values[index]
231        } else {
232            cell_properties_xml.and_then(|xml| direct_table_property(xml, name))
233        }
234    };
235    let shading_xml = if let Some(tokens) = &tokens {
236        tokens.values[0]
237    } else {
238        cell_properties_xml
239    };
240    let background_color = shading_xml
241        .and_then(|xml| {
242            re::get_unchecked(r#"(?i)<w:shd\b[^>]*w:fill="([^"]+)""#)
243                .captures(xml)
244                .and_then(|caps| caps.get(1).map(|value| value.as_str()))
245        })
246        .and_then(|value| normalize_hex_color(Some(value)));
247    let grid_span = property(1, "w:gridSpan").and_then(|tag| parse_integer_attribute(tag, "w:val"));
248    let preferred_width = property(2, "w:tcW").and_then(parse_table_preferred_width);
249    let width_twips = preferred_width.as_ref().and_then(preferred_width_twips);
250    let margin_twips = if let Some(tokens) = &tokens {
251        tokens.margin_spacing()
252    } else {
253        cell_properties_xml
254            .and_then(|xml| {
255                regex_find(xml, r"(?is)<w:tcMar\b[\s\S]*?</w:tcMar>|<w:tcMar\b[^>]*/?>")
256            })
257            .and_then(parse_table_box_spacing)
258    };
259    let borders = if let Some(tokens) = &tokens {
260        tokens.border_set()
261    } else {
262        cell_properties_xml
263            .and_then(|xml| {
264                regex_find(
265                    xml,
266                    r"(?is)<w:tcBorders\b[\s\S]*?</w:tcBorders>|<w:tcBorders\b[^>]*/?>",
267                )
268            })
269            .and_then(parse_table_border_set)
270    };
271    let vertical_align_tag = if let Some(tokens) = &tokens {
272        tokens.values[5]
273    } else {
274        cell_properties_xml.and_then(|xml| regex_find(xml, r"(?i)<w:vAlign\b[^>]*>"))
275    };
276    let vertical_align_raw =
277        vertical_align_tag.and_then(|tag| crate::xml::get_attribute_ref(tag, "w:val"));
278    let vertical_align = match vertical_align_raw {
279        Some(value) if value.eq_ignore_ascii_case("top") => Some(TableCellVerticalAlign::Top),
280        Some(value) if value.eq_ignore_ascii_case("center") => Some(TableCellVerticalAlign::Center),
281        Some(value) if value.eq_ignore_ascii_case("bottom") => Some(TableCellVerticalAlign::Bottom),
282        _ => None,
283    };
284    let v_merge_tag = if let Some(tokens) = &tokens {
285        tokens.values[6]
286    } else {
287        cell_properties_xml.and_then(|xml| regex_find(xml, r"(?i)<w:vMerge\b[^>]*/?>"))
288    };
289    let text_direction = property(7, "w:textDirection")
290        .and_then(|tag| get_attribute(tag, "w:val"))
291        .filter(|value| {
292            matches!(
293                value.as_str(),
294                "lrTb" | "tbRl" | "btLr" | "lrTbV" | "tbRlV" | "tbLrV"
295            )
296        });
297    let no_wrap = property(8, "w:noWrap").and_then(|tag| parse_on_off_attribute(tag, "noWrap"));
298    let v_merge_raw = v_merge_tag.and_then(|tag| crate::xml::get_attribute_ref(tag, "w:val"));
299    let v_merge = if v_merge_tag.is_some() {
300        Some(
301            if v_merge_raw.is_some_and(|value| value.eq_ignore_ascii_case("restart")) {
302                "restart".to_string()
303            } else {
304                "continue".to_string()
305            },
306        )
307    } else {
308        None
309    };
310    let has_cell_style = background_color.is_some()
311        || grid_span.is_some_and(|v| v > 1)
312        || preferred_width.is_some()
313        || width_twips.is_some()
314        || margin_twips.is_some()
315        || vertical_align.is_some()
316        || text_direction.is_some()
317        || no_wrap.is_some()
318        || borders.is_some();
319    ParsedTableCellResult {
320        cell: TableCellNode {
321            r#type: TableCellNodeType::TableCell,
322            style: if has_cell_style {
323                Some(TableCellStyle {
324                    background_color,
325                    grid_span: grid_span.filter(|&v| v > 1),
326                    row_span: None,
327                    v_merge_continuation: None,
328                    width_twips,
329                    source_width: Some(TableWidthSource {
330                        preferred_width: preferred_width.clone(),
331                        width_twips,
332                        inherited_preferred_width: None,
333                    }),
334                    preferred_width,
335                    margin_twips,
336                    vertical_align,
337                    source_text_direction: text_direction.clone(),
338                    text_direction,
339                    no_wrap,
340                    source_no_wrap: no_wrap,
341                    borders,
342                })
343            } else {
344                None
345            },
346            nodes: Vec::new(),
347        },
348        v_merge,
349    }
350}
351
352pub fn parse_table(
353    table_xml: &str,
354    context: &crate::parse::context::ParseContext<
355        '_,
356        impl std::borrow::Borrow<super::context::ParsedStyleSheet>,
357    >,
358) -> TableNode {
359    let table_properties_xml = direct_table_property(table_xml, "w:tblPr");
360    let table_style_id = table_properties_xml
361        .as_deref()
362        .and_then(|xml| direct_table_property(xml, "w:tblStyle"))
363        .and_then(|tag| get_attribute(tag, "w:val"));
364    let resolved_table_style_id = table_style_id
365        .as_deref()
366        .or(context.styles().default_table_style_id.as_deref());
367    let table_style =
368        resolved_table_style_id.and_then(|id| context.styles().table_style_by_id.get(id));
369    let table_paragraph_spacing = resolved_table_style_id
370        .and_then(|id| context.styles().table_paragraph_spacing_by_style_id.get(id));
371    let style_table_properties = table_style
372        .and_then(|style| style.conditions.get(&TableConditionalStyleType::WholeTable))
373        .and_then(|condition| condition.table_properties.clone());
374    let style_table_look = table_style
375        .and_then(|style| style.conditions.get(&TableConditionalStyleType::WholeTable))
376        .and_then(|condition| condition.table_look.clone());
377    let explicit_properties =
378        parse_table_style_properties_from_xml(table_properties_xml.as_deref());
379    let merged_properties = merge_table_style_properties(
380        style_table_properties.as_ref(),
381        explicit_properties.as_ref(),
382    );
383    let width_twips = merged_properties.as_ref().and_then(|p| p.width_twips);
384    let preferred_width = merged_properties
385        .as_ref()
386        .and_then(|p| p.preferred_width.clone());
387    let alignment = merged_properties.as_ref().and_then(|p| p.alignment);
388    let bidi_visual = merged_properties.as_ref().and_then(|p| p.bidi_visual);
389    let indent_twips = merged_properties.as_ref().and_then(|p| p.indent_twips);
390    let layout = merged_properties.as_ref().and_then(|p| p.layout);
391    let cell_spacing_twips = merged_properties
392        .as_ref()
393        .and_then(|p| p.cell_spacing_twips);
394    let floating = merged_properties.as_ref().and_then(|p| p.floating.clone());
395    let cell_margin_twips = merged_properties
396        .as_ref()
397        .and_then(|p| p.cell_margin_twips.clone());
398    let table_borders_xml = table_properties_xml.as_deref().and_then(|xml| {
399        regex_find(
400            xml,
401            r"(?is)<w:tblBorders\b[\s\S]*?</w:tblBorders>|<w:tblBorders\b[^>]*/?>",
402        )
403    });
404    let explicit_borders = table_borders_xml.and_then(parse_table_border_set);
405    let table_grid_xml = direct_table_property(table_xml, "w:tblGrid");
406    let column_widths_twips: Vec<i64> = table_grid_xml
407        .map(|grid| {
408            crate::xml::extract_direct_child_tag_ranges(grid)
409                .into_iter()
410                .filter(|range| range.tag_name.eq_ignore_ascii_case("w:gridCol"))
411                .filter_map(|range| parse_integer_attribute(&grid[range.start..range.end], "w:w"))
412                .filter(|&width| width >= 0)
413                .collect()
414        })
415        .unwrap_or_default();
416    let table_look = merge_table_look(
417        parse_table_look(table_properties_xml.as_deref()),
418        style_table_look.as_ref(),
419    );
420    let preserve_collapsed_base = layout == Some(TableLayout::Fixed)
421        && cell_spacing_twips.unwrap_or(0) == 0
422        && explicit_borders.is_none()
423        && table_style.is_none_or(|style| {
424            !style.has_row_cell_spacing
425                && style.conditions.iter().all(|(kind, condition)| {
426                    *kind == TableConditionalStyleType::WholeTable
427                        && condition.table_borders.is_none()
428                })
429        });
430    let row_blocks: Vec<_> = crate::xml::extract_balanced_tag_slices(table_xml, "w:tr")
431        .into_iter()
432        .filter(|row| {
433            direct_table_property(row, "w:trPr")
434                .and_then(|properties| direct_table_property(properties, "w:del"))
435                .is_none()
436        })
437        .collect();
438    let source_grid_bound = table_grid_xml
439        .map(|_| column_widths_twips.len() as i64)
440        .unwrap_or_else(|| {
441            row_blocks
442                .iter()
443                .map(|row| {
444                    crate::xml::extract_balanced_tag_slices(row, "w:tc")
445                        .iter()
446                        .fold(0i64, |total, cell| {
447                            let span = direct_table_property(cell, "w:tcPr")
448                                .and_then(|xml| direct_table_property(xml, "w:gridSpan"))
449                                .and_then(|tag| parse_integer_attribute(tag, "w:val"))
450                                .unwrap_or(1)
451                                .max(1);
452                            total.saturating_add(span)
453                        })
454                })
455                .max()
456                .unwrap_or(0)
457        });
458    let mut rows: Vec<TableRowNode> = Vec::new();
459    let mut cell_sources_by_row: Vec<Vec<&str>> = Vec::new();
460    #[derive(Clone, Copy)]
461    struct VerticalMergeAnchor {
462        row_index: usize,
463        cell_index: usize,
464    }
465    let mut active_vertical_merge_by_column: HashMap<i64, VerticalMergeAnchor> = HashMap::new();
466    let mut has_row_spacing_override = false;
467    let mut has_row_table_exception = false;
468    let cache_geometry = row_blocks.len() >= 12 && table_xml.len() >= 64 * 1024;
469    let mut geometry_cache = HashMap::<&str, ParsedTableCellResult>::new();
470    for row_xml in row_blocks {
471        let row_properties_xml = direct_table_property(row_xml, "w:trPr");
472        has_row_table_exception |= direct_table_property(row_xml, "w:tblPrEx").is_some();
473        if preserve_collapsed_base && !has_row_spacing_override {
474            has_row_spacing_override = row_properties_xml
475                .and_then(|properties| direct_table_property(properties, "w:tblCellSpacing"))
476                .is_some()
477                || direct_table_property(row_xml, "w:tblPrEx")
478                    .and_then(|properties| direct_table_property(properties, "w:tblCellSpacing"))
479                    .is_some();
480        }
481        let grid_before = row_properties_xml
482            .as_deref()
483            .and_then(|xml| direct_table_property(xml, "w:gridBefore"))
484            .and_then(|tag| parse_integer_attribute(tag, "w:val"))
485            .filter(|value| *value >= 0);
486        let grid_after = row_properties_xml
487            .as_deref()
488            .and_then(|xml| direct_table_property(xml, "w:gridAfter"))
489            .and_then(|tag| parse_integer_attribute(tag, "w:val"))
490            .filter(|value| *value >= 0);
491        let width_before = row_properties_xml
492            .as_deref()
493            .and_then(|xml| direct_table_property(xml, "w:wBefore"))
494            .and_then(parse_table_preferred_width);
495        let width_after = row_properties_xml
496            .as_deref()
497            .and_then(|xml| direct_table_property(xml, "w:wAfter"))
498            .and_then(parse_table_preferred_width);
499        let row_background_color = row_properties_xml
500            .as_deref()
501            .and_then(|xml| regex_capture(xml, r#"(?i)<w:shd\b[^>]*w:fill="([^"]+)""#))
502            .as_deref()
503            .and_then(|value| normalize_hex_color(Some(value)));
504        let row_height_tag = row_properties_xml
505            .as_deref()
506            .and_then(|xml| regex_tag(xml, r"(?i)<w:trHeight\b[^>]*>"));
507        let row_height_raw = row_height_tag
508            .as_deref()
509            .and_then(|tag| parse_integer_attribute(tag, "w:val"));
510        let row_height_twips = row_height_raw.filter(|&v| v > 0);
511        let row_height_rule_raw = row_height_tag
512            .as_deref()
513            .and_then(|tag| get_attribute(tag, "w:hRule"))
514            .map(|v| v.to_ascii_lowercase());
515        let row_height_rule = match row_height_rule_raw.as_deref() {
516            Some("atleast") => Some(TableRowHeightRule::AtLeast),
517            Some("exact") => Some(TableRowHeightRule::Exact),
518            Some("auto") => Some(TableRowHeightRule::Auto),
519            _ => None,
520        };
521        let row_cant_split = row_properties_xml
522            .as_deref()
523            .and_then(|xml| parse_on_off_attribute(xml, "cantSplit"));
524        let row_is_header = row_properties_xml
525            .as_deref()
526            .and_then(|xml| parse_on_off_attribute(xml, "tblHeader"));
527        let cell_sources = crate::xml::extract_balanced_tag_slices(row_xml, "w:tc");
528        let parsed_cells: Vec<ParsedTableCellResult> = cell_sources
529            .iter()
530            .map(|cell_xml| {
531                if !cache_geometry {
532                    return parse_table_cell_geometry(cell_xml);
533                }
534                let source = direct_table_property(cell_xml, "w:tcPr");
535                let Some(properties) = source.filter(|xml| {
536                    (16..=4096).contains(&xml.len())
537                }) else {
538                    return parse_table_cell_properties(source);
539                };
540                if let Some(parsed) = geometry_cache.get(properties) {
541                    return parsed.clone();
542                }
543                let parsed = parse_table_cell_properties(source);
544                if geometry_cache.len() < 32 {
545                    geometry_cache.insert(properties, parsed.clone());
546                }
547                parsed
548            })
549            .collect();
550        if parsed_cells.is_empty() {
551            continue;
552        }
553        cell_sources_by_row.push(cell_sources);
554        let mut cells = Vec::new();
555        let mut column_cursor = resolve_table_grid_skip_count(grid_before, source_grid_bound);
556        for parsed_cell in parsed_cells {
557            let mut cell = parsed_cell.cell;
558            let column_span = cell
559                .style
560                .as_ref()
561                .and_then(|s| s.grid_span)
562                .unwrap_or(1)
563                .max(1);
564            let start_column = column_cursor;
565            let end_column = start_column + column_span - 1;
566            column_cursor += column_span;
567            if parsed_cell.v_merge.as_deref() == Some("continue") {
568                let mut continuation_anchors = Vec::new();
569                for column_index in start_column..=end_column {
570                    if let Some(anchor) = active_vertical_merge_by_column.get(&column_index) {
571                        continuation_anchors.push(*anchor);
572                    }
573                }
574                continuation_anchors
575                    .sort_unstable_by_key(|anchor| (anchor.row_index, anchor.cell_index));
576                continuation_anchors.dedup_by_key(|anchor| (anchor.row_index, anchor.cell_index));
577                for anchor in continuation_anchors {
578                    if let Some(row) = rows.get_mut(anchor.row_index) {
579                        if let Some(anchor_cell) = row.cells.get_mut(anchor.cell_index) {
580                            let anchor_style = anchor_cell.style.get_or_insert(TableCellStyle {
581                                background_color: None,
582                                grid_span: None,
583                                row_span: None,
584                                v_merge_continuation: None,
585                                width_twips: None,
586                                preferred_width: None,
587                                source_width: None,
588                                margin_twips: None,
589                                vertical_align: None,
590                                text_direction: None,
591                                source_text_direction: None,
592                                no_wrap: None,
593                                source_no_wrap: None,
594                                borders: None,
595                            });
596                            anchor_style.row_span = Some(anchor_style.row_span.unwrap_or(1) + 1);
597                        }
598                    }
599                    for column_index in start_column..=end_column {
600                        active_vertical_merge_by_column.insert(column_index, anchor);
601                    }
602                }
603                let mut style = cell.style.unwrap_or(TableCellStyle {
604                    background_color: None,
605                    grid_span: None,
606                    row_span: None,
607                    v_merge_continuation: None,
608                    width_twips: None,
609                    preferred_width: None,
610                    source_width: None,
611                    margin_twips: None,
612                    vertical_align: None,
613                    text_direction: None,
614                    source_text_direction: None,
615                    no_wrap: None,
616                    source_no_wrap: None,
617                    borders: None,
618                });
619                style.v_merge_continuation = Some(true);
620                cell.style = Some(style);
621                cells.push(cell);
622                continue;
623            }
624            if parsed_cell.v_merge.as_deref() == Some("restart") {
625                let mut style = cell.style.unwrap_or(TableCellStyle {
626                    background_color: None,
627                    grid_span: None,
628                    row_span: None,
629                    v_merge_continuation: None,
630                    width_twips: None,
631                    preferred_width: None,
632                    source_width: None,
633                    margin_twips: None,
634                    vertical_align: None,
635                    text_direction: None,
636                    source_text_direction: None,
637                    no_wrap: None,
638                    source_no_wrap: None,
639                    borders: None,
640                });
641                style.row_span = Some(1);
642                cell.style = Some(style.clone());
643                let anchor = VerticalMergeAnchor {
644                    row_index: rows.len(),
645                    cell_index: cells.len(),
646                };
647                for column_index in start_column..=end_column {
648                    active_vertical_merge_by_column.insert(column_index, anchor);
649                }
650            } else {
651                for column_index in start_column..=end_column {
652                    active_vertical_merge_by_column.remove(&column_index);
653                }
654            }
655            cells.push(cell);
656        }
657        rows.push(TableRowNode {
658            r#type: TableRowNodeType::TableRow,
659            cells,
660            style: if grid_before.is_some()
661                || grid_after.is_some()
662                || width_before.is_some()
663                || width_after.is_some()
664                || row_background_color.is_some()
665                || row_height_twips.is_some()
666                || row_height_rule.is_some()
667                || row_cant_split.is_some()
668                || row_is_header.is_some()
669            {
670                Some(TableRowStyle {
671                    grid_before,
672                    grid_after,
673                    source_row_geometry: Some(TableRowGeometrySource {
674                        grid_before,
675                        grid_after,
676                        width_before: width_before.clone(),
677                        width_after: width_after.clone(),
678                    }),
679                    width_before,
680                    width_after,
681                    background_color: row_background_color,
682                    height_twips: row_height_twips,
683                    height_rule: row_height_rule,
684                    cant_split: row_cant_split,
685                    is_header: row_is_header,
686                })
687            } else {
688                None
689            },
690        });
691    }
692    let (column_widths_twips, collapsed_grid_columns) = if rows.iter().any(|row| {
693        row.style.as_ref().is_some_and(|style| {
694            style.grid_before.unwrap_or(0) > 0 || style.grid_after.unwrap_or(0) > 0
695        })
696    }) {
697        (column_widths_twips, None)
698    } else {
699        let preserve_collapsed = preserve_collapsed_base
700            && !has_row_spacing_override
701            && rows.iter().all(|row| {
702                row.style.as_ref().is_none_or(|style| {
703                    style.grid_before.unwrap_or(0) == 0 && style.grid_after.unwrap_or(0) == 0
704                })
705            });
706        normalize_conflicting_table_grid(
707            column_widths_twips,
708            &mut rows,
709            preserve_collapsed,
710            preserve_collapsed
711                && floating.is_none()
712                && !has_row_table_exception
713                && matches!(
714                    preferred_width.as_ref(),
715                    Some(crate::model::TablePreferredWidth::Auto)
716                ),
717        )
718    };
719    let column_count = column_widths_twips
720        .len()
721        .max(
722            rows.iter()
723                .map(|row| {
724                    row.cells.iter().fold(0i64, |total, cell| {
725                        total
726                            + cell
727                                .style
728                                .as_ref()
729                                .and_then(|s| s.grid_span)
730                                .filter(|&v| v > 1)
731                                .unwrap_or(1)
732                    }) + resolve_table_grid_skip_count(
733                        row.style.as_ref().and_then(|style| style.grid_before),
734                        source_grid_bound,
735                    ) + resolve_table_grid_skip_count(
736                        row.style.as_ref().and_then(|style| style.grid_after),
737                        source_grid_bound,
738                    )
739                })
740                .max()
741                .unwrap_or(0) as usize,
742        )
743        .max(1) as i64;
744    // Resolve cell geometry before parsing text so conditional table formatting
745    // enters the cascade below paragraph, character, and direct run properties.
746    {
747        let row_count = rows.len() as i64;
748        for (row_index, row) in rows.iter_mut().enumerate() {
749            let mut column_cursor = resolve_table_grid_skip_count(
750                row.style.as_ref().and_then(|style| style.grid_before),
751                source_grid_bound,
752            );
753            for (cell_index, cell) in row.cells.iter_mut().enumerate() {
754                let column_span = cell
755                    .style
756                    .as_ref()
757                    .and_then(|s| s.grid_span)
758                    .unwrap_or(1)
759                    .max(1);
760                let start_column_index = column_cursor;
761                let end_column_index = start_column_index + column_span - 1;
762                column_cursor += column_span;
763                let condition = table_style.and_then(|table_style| {
764                    resolve_table_condition_for_cell(
765                        table_style,
766                        &table_look,
767                        row_index as i64,
768                        row_count,
769                        start_column_index,
770                        end_column_index,
771                        column_count,
772                    )
773                });
774                cell.nodes = parse_table_cell_content_with_run_style(
775                    &cell_sources_by_row[row_index][cell_index],
776                    context,
777                    table_paragraph_spacing,
778                    condition
779                        .as_ref()
780                        .and_then(|value| value.run_style.as_ref()),
781                );
782                let Some(condition) = condition else { continue };
783                if cell
784                    .style
785                    .as_ref()
786                    .and_then(|style| style.preferred_width.as_ref())
787                    .is_none()
788                {
789                    if let Some(preferred_width) = condition.cell_preferred_width.as_ref() {
790                        let cell_style = cell.style.get_or_insert(TableCellStyle {
791                            background_color: None,
792                            grid_span: None,
793                            row_span: None,
794                            v_merge_continuation: None,
795                            width_twips: None,
796                            preferred_width: None,
797                            source_width: None,
798                            margin_twips: None,
799                            vertical_align: None,
800                            text_direction: None,
801                            source_text_direction: None,
802                            no_wrap: None,
803                            source_no_wrap: None,
804                            borders: None,
805                        });
806                        cell_style.preferred_width = Some(preferred_width.clone());
807                        cell_style.width_twips = preferred_width_twips(preferred_width);
808                    }
809                }
810                if let Some(style) = cell.style.as_mut() {
811                    style.source_width = Some(TableWidthSource {
812                        preferred_width: style.preferred_width.clone(),
813                        width_twips: style.width_twips,
814                        inherited_preferred_width: condition.cell_preferred_width.clone(),
815                    });
816                }
817                if cell
818                    .style
819                    .as_ref()
820                    .and_then(|s| s.v_merge_continuation)
821                    .unwrap_or(false)
822                {
823                    continue;
824                }
825                if condition.row_background_color.is_some()
826                    && row
827                        .style
828                        .as_ref()
829                        .and_then(|s| s.background_color.as_ref())
830                        .is_none()
831                {
832                    let row_style = row.style.get_or_insert(TableRowStyle {
833                        grid_before: None,
834                        grid_after: None,
835                        width_before: None,
836                        width_after: None,
837                        source_row_geometry: None,
838                        background_color: None,
839                        height_twips: None,
840                        height_rule: None,
841                        cant_split: None,
842                        is_header: None,
843                    });
844                    row_style.background_color = condition.row_background_color.clone();
845                }
846                if condition.cell_background_color.is_some()
847                    && cell
848                        .style
849                        .as_ref()
850                        .and_then(|s| s.background_color.as_ref())
851                        .is_none()
852                    && row
853                        .style
854                        .as_ref()
855                        .and_then(|s| s.background_color.as_ref())
856                        .is_none()
857                {
858                    let cell_style = cell.style.get_or_insert(TableCellStyle {
859                        background_color: None,
860                        grid_span: None,
861                        row_span: None,
862                        v_merge_continuation: None,
863                        width_twips: None,
864                        preferred_width: None,
865                        source_width: None,
866                        margin_twips: None,
867                        vertical_align: None,
868                        text_direction: None,
869                        source_text_direction: None,
870                        no_wrap: None,
871                        source_no_wrap: None,
872                        borders: None,
873                    });
874                    cell_style.background_color = condition.cell_background_color.clone();
875                }
876                if let Some(ref cell_borders) = condition.cell_borders {
877                    let cell_style = cell.style.get_or_insert(TableCellStyle {
878                        background_color: None,
879                        grid_span: None,
880                        row_span: None,
881                        v_merge_continuation: None,
882                        width_twips: None,
883                        preferred_width: None,
884                        source_width: None,
885                        margin_twips: None,
886                        vertical_align: None,
887                        text_direction: None,
888                        source_text_direction: None,
889                        no_wrap: None,
890                        source_no_wrap: None,
891                        borders: None,
892                    });
893                    cell_style.borders =
894                        merge_table_border_sets(Some(cell_borders), cell_style.borders.as_ref());
895                }
896                if let Some(text_alignment) = condition.paragraph_text_alignment {
897                    apply_text_alignment_to_table_cell_content(
898                        &mut cell.nodes,
899                        text_alignment,
900                        context,
901                    );
902                }
903                if let Some(snap_to_grid) = condition.paragraph_snap_to_grid {
904                    apply_snap_to_grid_to_table_cell_content(
905                        &mut cell.nodes,
906                        snap_to_grid,
907                        context,
908                    );
909                }
910                if let Some(paragraph_align) = condition.paragraph_align {
911                    apply_paragraph_alignment_to_table_cell_content(
912                        &mut cell.nodes,
913                        paragraph_align,
914                    );
915                }
916            }
917        }
918    }
919    let resolved_table_borders = merge_table_border_sets(
920        table_style
921            .and_then(|style| style.conditions.get(&TableConditionalStyleType::WholeTable))
922            .and_then(|condition| condition.table_borders.as_ref()),
923        explicit_borders.as_ref(),
924    );
925    for row in &mut rows {
926        for cell in &mut row.cells {
927            if let Some(style) = cell.style.as_mut() {
928                style.source_width = Some(TableWidthSource {
929                    preferred_width: style.preferred_width.clone(),
930                    width_twips: style.width_twips,
931                    inherited_preferred_width: style
932                        .source_width
933                        .as_ref()
934                        .and_then(|source| source.inherited_preferred_width.clone()),
935                });
936            }
937        }
938    }
939    let has_table_style = table_style_id.is_some()
940        || table_grid_xml.is_some()
941        || preferred_width.is_some()
942        || alignment.is_some()
943        || bidi_visual.is_some()
944        || width_twips.is_some()
945        || indent_twips.is_some()
946        || layout.is_some()
947        || cell_spacing_twips.is_some()
948        || floating.is_some()
949        || cell_margin_twips.is_some()
950        || !column_widths_twips.is_empty()
951        || resolved_table_borders.is_some();
952    let review_rows = crate::xml::extract_balanced_tag_slices(table_xml, "w:tr")
953        .into_iter().enumerate().filter_map(|(source_index, source)| {
954            let properties = direct_table_property(source, "w:trPr")?;
955            direct_table_property(properties, "w:del")?;
956            let current_properties = crate::xml::without_direct_child_tags(properties, "w:del");
957            let visible = crate::revisions::visible_removed_xml(&source.replacen(properties, &current_properties, 1));
958            let xml = format!("<w:tbl>{}{}{visible}</w:tbl>",
959                table_properties_xml.unwrap_or_default(), table_grid_xml.unwrap_or_default());
960            let mut parsed = parse_table(&xml, context);
961            Some(crate::revisions::ReviewRow { source_index, source_xml: source.to_owned(), row: parsed.rows.pop()? })
962        }).collect::<Vec<_>>();
963    TableNode {
964        review_rows: (!review_rows.is_empty()).then_some(review_rows),
965        r#type: TableNodeType::Table,
966        block_id: None,
967        rows,
968        style: if has_table_style {
969            Some(TableStyle {
970                style_id: table_style_id.clone(),
971                source_style_id: table_style_id,
972                width_twips,
973                source_width: Some(TableWidthSource {
974                    preferred_width: preferred_width.clone(),
975                    width_twips,
976                    inherited_preferred_width: style_table_properties
977                        .as_ref()
978                        .and_then(|properties| properties.preferred_width.clone()),
979                }),
980                preferred_width,
981                alignment,
982                source_alignment: alignment,
983                source_inherited_alignment: style_table_properties
984                    .as_ref()
985                    .and_then(|properties| properties.alignment),
986                bidi_visual,
987                source_bidi_visual: bidi_visual,
988                source_inherited_bidi_visual: style_table_properties
989                    .as_ref()
990                    .and_then(|properties| properties.bidi_visual),
991                indent_twips,
992                layout,
993                cell_spacing_twips,
994                cell_margin_twips,
995                column_widths_twips: if column_widths_twips.is_empty() && table_grid_xml.is_none() {
996                    None
997                } else {
998                    Some(column_widths_twips)
999                },
1000                collapsed_grid_columns,
1001                borders: resolved_table_borders,
1002                floating,
1003            })
1004        } else {
1005            None
1006        },
1007        source_xml: Some(table_xml.to_string()),
1008        source_text_patches: None,
1009    }
1010}
1011
1012/// Some generators emit a placeholder tblGrid while each row's real geometry
1013/// lives in per-cell tcW values (rows may even have differing boundaries).
1014/// Preserve the existing physical reconstruction when preferred widths differ
1015/// from the declared grid, while retaining compatible logical boundaries.
1016fn normalize_conflicting_table_grid(
1017    grid: Vec<i64>,
1018    rows: &mut [TableRowNode],
1019    preserve_collapsed_columns: bool,
1020    resolve_shared_constraints: bool,
1021) -> (Vec<i64>, Option<Vec<usize>>) {
1022    const BOUNDARY_TOLERANCE_TWIPS: i64 = 80;
1023    if grid.is_empty() || rows.is_empty() {
1024        return (grid, None);
1025    }
1026
1027    let span_of = |cell: &TableCellNode| -> usize {
1028        cell.style
1029            .as_ref()
1030            .and_then(|style| style.grid_span)
1031            .unwrap_or(1)
1032            .max(1) as usize
1033    };
1034    let width_of = |cell: &TableCellNode| -> Option<i64> {
1035        cell.style
1036            .as_ref()
1037            .and_then(|style| style.width_twips)
1038            .filter(|&width| width > 0)
1039    };
1040    let is_continuation = |cell: &TableCellNode| -> bool {
1041        cell.style
1042            .as_ref()
1043            .and_then(|style| style.v_merge_continuation)
1044            .unwrap_or(false)
1045    };
1046
1047    // Only regrid when the declared grid is structurally consistent with the
1048    // rows (cell spans actually address its columns) but width-wrong. When the
1049    // grid is structurally bogus (e.g. one gridCol for a three-column table),
1050    // the row-derived fallbacks downstream already model Word's behavior.
1051    let max_span_sum = rows
1052        .iter()
1053        .map(|row| row.cells.iter().map(|cell| span_of(cell)).sum::<usize>())
1054        .max()
1055        .unwrap_or(0);
1056    if max_span_sum != grid.len() {
1057        return (grid, None);
1058    }
1059
1060    // Generators that slice page layouts into (nested) tables emit uniform
1061    // placeholder grids — equal divisions of the table width — while the real
1062    // geometry lives in per-cell tcW values. Small per-cell deviations slip
1063    // under the conflict ratio below yet still misplace the text fragments the
1064    // cells position, so when the declared grid is uniform and every cell
1065    // carries an explicit width, trust the cell widths outright.
1066    let grid_is_uniform = grid.len() > 1 && grid.iter().all(|&width| (width - grid[0]).abs() <= 1);
1067    let cells_fully_measured = rows.iter().all(|row| {
1068        row.cells
1069            .iter()
1070            .all(|cell| is_continuation(cell) || width_of(cell).is_some())
1071    });
1072    let uniform_placeholder_grid = grid_is_uniform && cells_fully_measured;
1073
1074    // Conflict detection: do explicit cell widths disagree with the grid?
1075    let mut measured_rows = 0usize;
1076    let mut conflict_rows = 0usize;
1077    let mut has_width_difference = false;
1078    for row in rows.iter() {
1079        let mut cursor = 0usize;
1080        let mut measured = 0usize;
1081        let mut conflicts = 0usize;
1082        for cell in &row.cells {
1083            let span = span_of(cell);
1084            let expected: i64 = grid.iter().skip(cursor).take(span).sum();
1085            cursor += span;
1086            // Continuation cells inherit the anchor row's geometry, and
1087            // generators often stamp them with placeholder widths copied from
1088            // the bogus grid — they are evidence of nothing.
1089            if is_continuation(cell) {
1090                continue;
1091            }
1092            let Some(actual) = width_of(cell) else {
1093                continue;
1094            };
1095            has_width_difference |= actual != expected;
1096            if expected <= 0 {
1097                continue;
1098            }
1099            measured += 1;
1100            if (actual - expected).abs() * 5 > expected {
1101                conflicts += 1;
1102            }
1103        }
1104        if measured > 0 {
1105            measured_rows += 1;
1106            if conflicts * 2 > measured {
1107                conflict_rows += 1;
1108            }
1109        }
1110    }
1111    // Retain shared columns when the preferred widths already agree. A union
1112    // of cell boundaries can otherwise erase columns inside spanning cells.
1113    if !has_width_difference
1114        || measured_rows == 0
1115        || (!uniform_placeholder_grid && conflict_rows * 2 <= measured_rows)
1116    {
1117        return (grid, None);
1118    }
1119
1120    // Union of every row's cumulative cell boundaries; cells without an
1121    // explicit width fall back to the original grid width for their span.
1122    // Vertically merged cells must share the anchor row's width: HTML rowspan
1123    // cannot express per-row geometry, so continuation cells adopt the
1124    // anchor's width and the difference is absorbed by the cells that follow
1125    // (matching how LibreOffice resolves such tables).
1126    let bucket_of = |position: i64| -> i64 {
1127        (position + BOUNDARY_TOLERANCE_TWIPS / 2) / BOUNDARY_TOLERANCE_TWIPS
1128    };
1129    let mut anchor_width_by_bucket: HashMap<i64, i64> = HashMap::new();
1130    let mut boundaries: Vec<i64> = vec![0];
1131    for row in rows.iter() {
1132        let mut cursor = 0usize;
1133        let mut position = 0i64;
1134        for cell in &row.cells {
1135            let span = span_of(cell);
1136            let fallback: i64 = grid.iter().skip(cursor).take(span).sum();
1137            cursor += span;
1138            let own_width = width_of(cell).unwrap_or(fallback.max(1));
1139            let bucket = bucket_of(position);
1140            let width = if is_continuation(cell) {
1141                *anchor_width_by_bucket.get(&bucket).unwrap_or(&own_width)
1142            } else {
1143                anchor_width_by_bucket.insert(bucket, own_width);
1144                own_width
1145            };
1146            position += width;
1147            boundaries.push(position);
1148        }
1149    }
1150    boundaries.sort_unstable();
1151    let mut merged: Vec<i64> = Vec::new();
1152    for boundary in boundaries {
1153        match merged.last() {
1154            Some(&last) if boundary - last <= BOUNDARY_TOLERANCE_TWIPS => {}
1155            _ => merged.push(boundary),
1156        }
1157    }
1158    if merged.len() < 2 {
1159        return (grid, None);
1160    }
1161
1162    let snap = |value: i64| -> usize {
1163        match merged.binary_search(&value) {
1164            Ok(index) => index,
1165            Err(index) => {
1166                if index == 0 {
1167                    0
1168                } else if index >= merged.len() {
1169                    merged.len() - 1
1170                } else if value - merged[index - 1] <= merged[index] - value {
1171                    index - 1
1172                } else {
1173                    index
1174                }
1175            }
1176        }
1177    };
1178
1179    let mut source_boundaries = vec![None; grid.len() + 1];
1180    let mut source_topology_consistent = true;
1181    let mut resolved_spans = Vec::new();
1182    anchor_width_by_bucket.clear();
1183    for row in rows.iter() {
1184        let mut cursor = 0usize;
1185        let mut position = 0i64;
1186        let mut resolved_cursor = 0usize;
1187        for cell in &row.cells {
1188            let span = span_of(cell);
1189            let source_start = cursor;
1190            let fallback: i64 = grid.iter().skip(cursor).take(span).sum();
1191            cursor += span;
1192            let own_width = width_of(cell).unwrap_or(fallback.max(1));
1193            let bucket = bucket_of(position);
1194            let width = if is_continuation(cell) {
1195                *anchor_width_by_bucket.get(&bucket).unwrap_or(&own_width)
1196            } else {
1197                anchor_width_by_bucket.insert(bucket, own_width);
1198                own_width
1199            };
1200            let start_index = snap(position);
1201            position += width;
1202            let end_index = snap(position).max(start_index + 1);
1203            let new_span = (end_index - start_index) as i64;
1204            resolved_spans.push(new_span);
1205            if start_index != resolved_cursor || end_index >= merged.len() {
1206                source_topology_consistent = false;
1207            } else {
1208                for (column, boundary) in [
1209                    (source_start, merged[start_index]),
1210                    (cursor, merged[end_index]),
1211                ] {
1212                    match source_boundaries.get_mut(column) {
1213                        Some(value @ None) => *value = Some(boundary),
1214                        Some(Some(value)) if *value == boundary => {}
1215                        _ => source_topology_consistent = false,
1216                    }
1217                }
1218            }
1219            resolved_cursor += new_span as usize;
1220        }
1221    }
1222    // Keep logical columns when all rows agree on their resolved boundaries.
1223    // Interior columns may be unobserved because every row spans over them.
1224    if source_topology_consistent {
1225        if let Some(resolved_grid) =
1226            preserve_shared_grid_topology(&grid, &source_boundaries, preserve_collapsed_columns)
1227        {
1228            let collapsed: Vec<_> = resolved_grid
1229                .iter()
1230                .enumerate()
1231                .filter_map(|(column, &width)| (width == 0).then_some(column))
1232                .collect();
1233            if collapsed.is_empty()
1234                || resolved_grid
1235                    .iter()
1236                    .copied()
1237                    .filter(|&width| width > 0)
1238                    .eq(merged.windows(2).map(|pair| pair[1] - pair[0]))
1239            {
1240                return (resolved_grid, (!collapsed.is_empty()).then_some(collapsed));
1241            }
1242        }
1243    }
1244
1245    if !source_topology_consistent && resolve_shared_constraints {
1246        if let Some(widths) = resolve_shared_column_preferences(grid.len(), rows) {
1247            return (widths, None);
1248        }
1249    }
1250
1251    let reduces_merged_span = rows
1252        .iter()
1253        .flat_map(|row| &row.cells)
1254        .zip(&resolved_spans)
1255        .any(|(cell, &resolved_span)| {
1256            let source_span = span_of(cell);
1257            source_span > 1 && resolved_span < source_span as i64
1258        });
1259    if reduces_merged_span {
1260        if let Some(widths) = resolve_repeated_unmerged_grid(&grid, rows) {
1261            return (widths, None);
1262        }
1263    }
1264
1265    let mut resolved_spans = resolved_spans.into_iter();
1266    for row in rows.iter_mut() {
1267        for cell in &mut row.cells {
1268            let new_span = resolved_spans.next().unwrap_or(1);
1269            if new_span > 1 {
1270                let style = cell.style.get_or_insert(TableCellStyle {
1271                    background_color: None,
1272                    grid_span: None,
1273                    row_span: None,
1274                    v_merge_continuation: None,
1275                    width_twips: None,
1276                    preferred_width: None,
1277                    source_width: None,
1278                    margin_twips: None,
1279                    vertical_align: None,
1280                    text_direction: None,
1281                    source_text_direction: None,
1282                    no_wrap: None,
1283                    source_no_wrap: None,
1284                    borders: None,
1285                });
1286                style.grid_span = Some(new_span);
1287            } else if let Some(style) = cell.style.as_mut() {
1288                style.grid_span = None;
1289            }
1290        }
1291    }
1292
1293    (
1294        merged.windows(2).map(|pair| pair[1] - pair[0]).collect(),
1295        None,
1296    )
1297}
1298
1299/// Repeated complete rows can establish shared columns independently of a
1300/// spanning cell's undersized preferred width. Keep their measured widths
1301/// without turning that preference into a shorter logical span.
1302fn resolve_repeated_unmerged_grid(grid: &[i64], rows: &[TableRowNode]) -> Option<Vec<i64>> {
1303    if grid.is_empty() || grid.iter().any(|&width| width <= 0) {
1304        return None;
1305    }
1306    let measured_width = |cell: &TableCellNode| -> Option<i64> {
1307        let style = cell.style.as_ref()?;
1308        if !matches!(
1309            style.preferred_width.as_ref(),
1310            Some(crate::model::TablePreferredWidth::Dxa { .. })
1311        ) {
1312            return None;
1313        }
1314        style.width_twips.filter(|&width| width > 0)
1315    };
1316    let mut anchored_widths: Option<Vec<i64>> = None;
1317    let mut independent_rows = 0usize;
1318    for row in rows {
1319        if row.style.as_ref().is_some_and(|style| {
1320            style.grid_before.unwrap_or(0) != 0 || style.grid_after.unwrap_or(0) != 0
1321        }) {
1322            return None;
1323        }
1324        if row.cells.len() != grid.len() {
1325            continue;
1326        }
1327        let widths = row.cells.iter().map(|cell| {
1328            let style = cell.style.as_ref()?;
1329            if style.grid_span.unwrap_or(1) != 1
1330                || style.row_span.is_some()
1331                || style.v_merge_continuation.unwrap_or(false)
1332            {
1333                return None;
1334            }
1335            measured_width(cell)
1336        });
1337        match &anchored_widths {
1338            Some(anchored) => {
1339                let agrees = widths
1340                    .zip(anchored)
1341                    .try_fold(true, |same, (width, &expected)| {
1342                        width.map(|actual| same && actual == expected)
1343                    });
1344                match agrees {
1345                    Some(true) => {}
1346                    Some(false) => return None,
1347                    None => continue,
1348                }
1349            }
1350            None => {
1351                let Some(widths) = widths.collect() else {
1352                    continue;
1353                };
1354                anchored_widths = Some(widths);
1355            }
1356        }
1357        independent_rows += 1;
1358    }
1359    if independent_rows < 2 {
1360        return None;
1361    }
1362    let anchored_widths = anchored_widths?;
1363    let mut preceding_merges = Vec::new();
1364    let mut next_merges = Vec::new();
1365    for row in rows {
1366        if row.cells.is_empty() {
1367            return None;
1368        }
1369        next_merges.clear();
1370        let mut start = 0usize;
1371        for cell in &row.cells {
1372            let style = cell.style.as_ref()?;
1373            let span = usize::try_from(style.grid_span.unwrap_or(1)).ok()?;
1374            if span == 0 {
1375                return None;
1376            }
1377            let end = start.checked_add(span)?;
1378            let columns = anchored_widths.get(start..end)?;
1379            if style.v_merge_continuation.unwrap_or(false) {
1380                if preceding_merges.binary_search(&(start, end)).is_err() {
1381                    return None;
1382                }
1383                next_merges.push((start, end));
1384            } else {
1385                if style.row_span.is_some_and(|span| span > 0) {
1386                    next_merges.push((start, end));
1387                }
1388                let expected = columns
1389                    .iter()
1390                    .try_fold(0i64, |total, &width| total.checked_add(width))?;
1391                let actual = measured_width(cell)?;
1392                if actual > expected || (span == 1 && actual != expected) {
1393                    return None;
1394                }
1395            }
1396            start = end;
1397        }
1398        if start != grid.len() {
1399            return None;
1400        }
1401        std::mem::swap(&mut preceding_merges, &mut next_merges);
1402    }
1403    Some(anchored_widths)
1404}
1405
1406/// Resolve fully anchored cell preferences on the shared logical columns.
1407fn resolve_shared_column_preferences(columns: usize, rows: &[TableRowNode]) -> Option<Vec<i64>> {
1408    let mut constraints: Vec<Vec<(usize, i64)>> = vec![Vec::new(); columns + 1];
1409    let mut anchored = vec![false; columns];
1410    for row in rows {
1411        let mut start = 0usize;
1412        for cell in &row.cells {
1413            let style = cell.style.as_ref()?;
1414            let span = usize::try_from(style.grid_span.unwrap_or(1)).ok()?;
1415            if span == 0 {
1416                return None;
1417            }
1418            let end = start.checked_add(span)?;
1419            if end > columns {
1420                return None;
1421            }
1422            if !style.v_merge_continuation.unwrap_or(false) {
1423                if !matches!(
1424                    style.preferred_width.as_ref(),
1425                    Some(crate::model::TablePreferredWidth::Dxa { .. })
1426                ) {
1427                    return None;
1428                }
1429                let width = style.width_twips.filter(|&value| value > 0)?;
1430                constraints[end].push((start, width));
1431                if span == 1 {
1432                    anchored[start] = true;
1433                }
1434            }
1435            start = end;
1436        }
1437        if start != columns {
1438            return None;
1439        }
1440    }
1441    if anchored.iter().any(|&value| !value) {
1442        return None;
1443    }
1444    let mut boundaries = vec![0i64; columns + 1];
1445    for end in 1..=columns {
1446        let mut position = boundaries[end - 1];
1447        for &(start, width) in &constraints[end] {
1448            position = position.max(boundaries[start].checked_add(width)?);
1449        }
1450        boundaries[end] = position;
1451    }
1452    Some(
1453        boundaries
1454            .windows(2)
1455            .map(|pair| pair[1] - pair[0])
1456            .collect(),
1457    )
1458}
1459
1460fn preserve_shared_grid_topology(
1461    grid: &[i64],
1462    boundaries: &[Option<i64>],
1463    allow_collapsed: bool,
1464) -> Option<Vec<i64>> {
1465    if boundaries.first().copied().flatten() != Some(0)
1466        || boundaries.last().copied().flatten().is_none()
1467        || grid.iter().any(|&width| width <= 0)
1468    {
1469        return None;
1470    }
1471    let minimum_column_width = if allow_collapsed { 0 } else { 1 };
1472    let mut result = Vec::with_capacity(grid.len());
1473    let mut start = 0usize;
1474    let mut position = 0i64;
1475    for (end, boundary) in boundaries.iter().enumerate().skip(1) {
1476        let Some(boundary) = *boundary else { continue };
1477        let width = boundary.checked_sub(position)?;
1478        if width < minimum_column_width * (end - start) as i64 {
1479            return None;
1480        }
1481        let source_width: i128 = grid[start..end].iter().map(|&value| value as i128).sum();
1482        let mut source_position = 0i128;
1483        let mut resolved_position = 0i64;
1484        for &column_width in &grid[start..end] {
1485            source_position += column_width as i128;
1486            let next_position = (source_position * width as i128 / source_width) as i64;
1487            let resolved_width = next_position - resolved_position;
1488            if resolved_width < minimum_column_width {
1489                return None;
1490            }
1491            result.push(resolved_width);
1492            resolved_position = next_position;
1493        }
1494        start = end;
1495        position = boundary;
1496    }
1497    Some(result)
1498}
1499
1500fn parse_table_look(table_properties_xml: Option<&str>) -> Option<ParsedTableLook> {
1501    let table_properties_xml = table_properties_xml?;
1502    let table_look_tag =
1503        regex_tag(table_properties_xml, r"(?i)<w:tblLook\b[^>]*/?>").unwrap_or_default();
1504    if table_look_tag.is_empty() {
1505        return None;
1506    }
1507    let look_mask_raw = get_attribute(&table_look_tag, "w:val");
1508    let look_mask = look_mask_raw.and_then(|v| i64::from_str_radix(&v, 16).ok());
1509    let row_band_size_tag = regex_tag(
1510        table_properties_xml,
1511        r"(?i)<w:tblStyleRowBandSize\b[^>]*/?>",
1512    )
1513    .or_else(|| {
1514        extract_balanced_tag_blocks(table_properties_xml, "w:tblStyleRowBandSize")
1515            .into_iter()
1516            .next()
1517    });
1518    let col_band_size_tag = regex_tag(
1519        table_properties_xml,
1520        r"(?i)<w:tblStyleColBandSize\b[^>]*/?>",
1521    )
1522    .or_else(|| {
1523        extract_balanced_tag_blocks(table_properties_xml, "w:tblStyleColBandSize")
1524            .into_iter()
1525            .next()
1526    });
1527    let row_band_size_raw = row_band_size_tag
1528        .as_deref()
1529        .and_then(|tag| parse_integer_attribute(tag, "w:val"))
1530        .unwrap_or(1)
1531        .max(1);
1532    let col_band_size_raw = col_band_size_tag
1533        .as_deref()
1534        .and_then(|tag| parse_integer_attribute(tag, "w:val"))
1535        .unwrap_or(1)
1536        .max(1);
1537    let resolve_on_off = |attribute: &str| -> Option<bool> {
1538        let value = get_attribute(&table_look_tag, attribute)?.to_ascii_lowercase();
1539        match value.as_str() {
1540            "1" | "true" | "on" => Some(true),
1541            "0" | "false" | "off" => Some(false),
1542            _ => None,
1543        }
1544    };
1545    let mask_value = look_mask.unwrap_or(0);
1546    Some(ParsedTableLook {
1547        first_row: resolve_on_off("w:firstRow").unwrap_or(mask_value & 0x0020 != 0),
1548        last_row: resolve_on_off("w:lastRow").unwrap_or(mask_value & 0x0040 != 0),
1549        first_col: resolve_on_off("w:firstColumn").unwrap_or(mask_value & 0x0080 != 0),
1550        last_col: resolve_on_off("w:lastColumn").unwrap_or(mask_value & 0x0100 != 0),
1551        no_h_band: resolve_on_off("w:noHBand").unwrap_or(mask_value & 0x0200 != 0),
1552        no_v_band: resolve_on_off("w:noVBand").unwrap_or(mask_value & 0x0400 != 0),
1553        row_band_size: row_band_size_raw,
1554        col_band_size: col_band_size_raw,
1555    })
1556}
1557
1558fn merge_table_look(
1559    direct: Option<ParsedTableLook>,
1560    inherited: Option<&ParsedTableLook>,
1561) -> ParsedTableLook {
1562    let mut merged = default_table_look();
1563    if let Some(inherited) = inherited {
1564        merged = inherited.clone();
1565    }
1566    if let Some(direct) = direct {
1567        merged = direct;
1568    }
1569    merged
1570}
1571
1572pub(crate) fn resolve_table_grid_skip_count(count: Option<i64>, bound: i64) -> i64 {
1573    count
1574        .filter(|value| *value >= 0 && *value <= bound)
1575        .unwrap_or(0)
1576}
1577
1578pub(crate) fn table_grid_column_bound(table: &TableNode) -> i64 {
1579    if let Some(widths) = table
1580        .style
1581        .as_ref()
1582        .and_then(|style| style.column_widths_twips.as_ref())
1583    {
1584        widths.len() as i64
1585    } else {
1586        table
1587            .rows
1588            .iter()
1589            .map(|row| {
1590                row.cells.iter().fold(0i64, |total, cell| {
1591                    total.saturating_add(
1592                        cell.style
1593                            .as_ref()
1594                            .and_then(|style| style.grid_span)
1595                            .unwrap_or(1)
1596                            .max(1),
1597                    )
1598                })
1599            })
1600            .max()
1601            .unwrap_or(0)
1602    }
1603}
1604
1605pub(crate) fn resolve_table_condition_for_cell(
1606    table_style: &ParsedTableStyleDefinition,
1607    table_look: &ParsedTableLook,
1608    row_index: i64,
1609    row_count: i64,
1610    start_column_index: i64,
1611    end_column_index: i64,
1612    column_count: i64,
1613) -> Option<ParsedTableStyleCondition> {
1614    let mut condition_types = vec![TableConditionalStyleType::WholeTable];
1615    let is_first_row = row_index == 0;
1616    let is_last_row = row_index == row_count - 1;
1617    let is_first_column = start_column_index == 0;
1618    let is_last_column = end_column_index >= column_count - 1;
1619    let row_band_size = table_look.row_band_size.max(1);
1620    let col_band_size = table_look.col_band_size.max(1);
1621    if !table_look.no_h_band {
1622        let band_row_index = row_index - if table_look.first_row { 1 } else { 0 };
1623        if band_row_index >= 0 {
1624            let band_row_group = band_row_index / row_band_size;
1625            condition_types.push(if band_row_group % 2 == 0 {
1626                TableConditionalStyleType::Band1Horz
1627            } else {
1628                TableConditionalStyleType::Band2Horz
1629            });
1630        }
1631    }
1632    if !table_look.no_v_band {
1633        let band_column_index = start_column_index - if table_look.first_col { 1 } else { 0 };
1634        if band_column_index >= 0 {
1635            let band_column_group = band_column_index / col_band_size;
1636            condition_types.push(if band_column_group % 2 == 0 {
1637                TableConditionalStyleType::Band1Vert
1638            } else {
1639                TableConditionalStyleType::Band2Vert
1640            });
1641        }
1642    }
1643    if table_look.first_row && is_first_row {
1644        condition_types.push(TableConditionalStyleType::FirstRow);
1645    }
1646    if table_look.last_row && is_last_row {
1647        condition_types.push(TableConditionalStyleType::LastRow);
1648    }
1649    if table_look.first_col && is_first_column {
1650        condition_types.push(TableConditionalStyleType::FirstCol);
1651    }
1652    if table_look.last_col && is_last_column {
1653        condition_types.push(TableConditionalStyleType::LastCol);
1654    }
1655    if table_look.first_row && table_look.first_col && is_first_row && is_first_column {
1656        condition_types.push(TableConditionalStyleType::NwCell);
1657    }
1658    if table_look.first_row && table_look.last_col && is_first_row && is_last_column {
1659        condition_types.push(TableConditionalStyleType::NeCell);
1660    }
1661    if table_look.last_row && table_look.first_col && is_last_row && is_first_column {
1662        condition_types.push(TableConditionalStyleType::SwCell);
1663    }
1664    if table_look.last_row && table_look.last_col && is_last_row && is_last_column {
1665        condition_types.push(TableConditionalStyleType::SeCell);
1666    }
1667    let mut resolved_condition: Option<ParsedTableStyleCondition> = None;
1668    for condition_type in condition_types {
1669        resolved_condition = merge_table_conditional_style(
1670            resolved_condition.as_ref(),
1671            table_style.conditions.get(&condition_type),
1672        );
1673    }
1674    resolved_condition
1675}
1676
1677fn merge_table_conditional_style(
1678    inherited: Option<&ParsedTableStyleCondition>,
1679    direct: Option<&ParsedTableStyleCondition>,
1680) -> Option<ParsedTableStyleCondition> {
1681    if inherited.is_none() && direct.is_none() {
1682        return None;
1683    }
1684    let merged = ParsedTableStyleCondition {
1685        row_background_color: direct
1686            .and_then(|d| d.row_background_color.clone())
1687            .or_else(|| inherited.and_then(|i| i.row_background_color.clone())),
1688        cell_background_color: direct
1689            .and_then(|d| d.cell_background_color.clone())
1690            .or_else(|| inherited.and_then(|i| i.cell_background_color.clone())),
1691        cell_preferred_width: direct
1692            .and_then(|value| value.cell_preferred_width.clone())
1693            .or_else(|| inherited.and_then(|value| value.cell_preferred_width.clone())),
1694        paragraph_align: direct
1695            .and_then(|d| d.paragraph_align)
1696            .or(inherited.and_then(|i| i.paragraph_align)),
1697        paragraph_text_alignment: direct
1698            .and_then(|value| value.paragraph_text_alignment)
1699            .or_else(|| inherited.and_then(|value| value.paragraph_text_alignment)),
1700        paragraph_snap_to_grid: direct
1701            .and_then(|value| value.paragraph_snap_to_grid)
1702            .or_else(|| inherited.and_then(|value| value.paragraph_snap_to_grid)),
1703        run_style: merge_text_styles(&[
1704            inherited.and_then(|i| i.run_style.clone()),
1705            direct.and_then(|d| d.run_style.clone()),
1706        ]),
1707        table_borders: merge_table_border_sets(
1708            inherited.and_then(|i| i.table_borders.as_ref()),
1709            direct.and_then(|d| d.table_borders.as_ref()),
1710        ),
1711        cell_borders: merge_table_border_sets(
1712            inherited.and_then(|i| i.cell_borders.as_ref()),
1713            direct.and_then(|d| d.cell_borders.as_ref()),
1714        ),
1715        table_properties: None,
1716        table_look: None,
1717    };
1718    if merged.row_background_color.is_none()
1719        && merged.cell_background_color.is_none()
1720        && merged.cell_preferred_width.is_none()
1721        && merged.paragraph_align.is_none()
1722        && merged.paragraph_text_alignment.is_none()
1723        && merged.paragraph_snap_to_grid.is_none()
1724        && merged.run_style.is_none()
1725        && merged.table_borders.is_none()
1726        && merged.cell_borders.is_none()
1727    {
1728        return None;
1729    }
1730    Some(merged)
1731}
1732
1733fn apply_snap_to_grid_to_table_cell_content(
1734    nodes: &mut [TableCellContentNode],
1735    table_snap_to_grid: bool,
1736    context: &crate::parse::context::ParseContext<
1737        '_,
1738        impl std::borrow::Borrow<super::context::ParsedStyleSheet>,
1739    >,
1740) {
1741    for node in nodes {
1742        let TableCellContentNode::Paragraph(paragraph) = node else {
1743            continue;
1744        };
1745        let properties = paragraph
1746            .source_xml
1747            .as_deref()
1748            .and_then(|xml| direct_table_property(xml, "w:pPr"))
1749            .unwrap_or_default();
1750        let direct_property = direct_table_property(properties, "w:snapToGrid");
1751        let direct =
1752            direct_property.and_then(|tag| crate::xml::parse_on_off_attribute(tag, "snapToGrid"));
1753        let explicit_style = direct_table_property(properties, "w:pStyle")
1754            .and_then(|tag| get_attribute(tag, "w:val"))
1755            .and_then(|id| context.styles().paragraph_style_by_id.get(&id))
1756            .filter(|style| style.source_has_snap_to_grid != Some(false))
1757            .and_then(|style| style.snap_to_grid);
1758        let inherited = explicit_style.or(Some(table_snap_to_grid));
1759        let style = paragraph.style.get_or_insert_with(Default::default);
1760        style.snap_to_grid = direct.or(inherited);
1761        style.source_snap_to_grid = style.snap_to_grid;
1762        style.source_has_snap_to_grid = Some(direct_property.is_some());
1763        style.source_inherited_snap_to_grid = inherited;
1764        style.source_table_snap_to_grid = Some(table_snap_to_grid);
1765    }
1766}
1767
1768fn apply_text_alignment_to_table_cell_content(
1769    nodes: &mut [TableCellContentNode],
1770    table_alignment: crate::model::ParagraphTextAlignment,
1771    context: &crate::parse::context::ParseContext<
1772        '_,
1773        impl std::borrow::Borrow<super::context::ParsedStyleSheet>,
1774    >,
1775) {
1776    for node in nodes {
1777        let TableCellContentNode::Paragraph(paragraph) = node else {
1778            continue;
1779        };
1780        let properties = paragraph
1781            .source_xml
1782            .as_deref()
1783            .and_then(|xml| direct_table_property(xml, "w:pPr"))
1784            .unwrap_or_default();
1785        let direct = super::style::parse_paragraph_text_alignment_from_xml(properties);
1786        let explicit_style = direct_table_property(properties, "w:pStyle")
1787            .and_then(|tag| get_attribute(tag, "w:val"))
1788            .and_then(|id| context.styles().paragraph_style_by_id.get(&id))
1789            .filter(|style| style.source_has_text_alignment != Some(false))
1790            .and_then(|style| style.text_alignment);
1791        let inherited = explicit_style.or(Some(table_alignment));
1792        let style = paragraph.style.get_or_insert(crate::model::ParagraphStyle {
1793            source_before_auto_spacing: None,
1794            source_after_auto_spacing: None,
1795            align: None,
1796            snap_to_grid: None,
1797            source_snap_to_grid: None,
1798            source_has_snap_to_grid: None,
1799            source_inherited_snap_to_grid: None,
1800            source_table_snap_to_grid: None,
1801            text_alignment: None,
1802            source_text_alignment: None,
1803            source_has_text_alignment: None,
1804            source_inherited_text_alignment: None,
1805            source_table_text_alignment: None,
1806            heading_level: None,
1807            style_id: None,
1808            style_name: None,
1809            numbering: None,
1810            spacing: None,
1811            indent: None,
1812            background_color: None,
1813            borders: None,
1814            tab_stops: None,
1815            contextual_spacing: None,
1816            keep_next: None,
1817            keep_lines: None,
1818            widow_control: None,
1819            page_break_before: None,
1820            drop_cap: None,
1821        }.into());
1822        style.text_alignment = direct.or(inherited);
1823        style.source_has_text_alignment =
1824            Some(direct_table_property(properties, "w:textAlignment").is_some());
1825        style.source_text_alignment = style.text_alignment;
1826        style.source_inherited_text_alignment = inherited;
1827        style.source_table_text_alignment = Some(table_alignment);
1828    }
1829}
1830
1831fn paragraph_has_direct_alignment(paragraph: &crate::model::ParagraphNode) -> bool {
1832    let Some(source_xml) = paragraph.source_xml.as_deref() else {
1833        return false;
1834    };
1835    let paragraph_properties_xml = extract_balanced_tag_blocks(source_xml, "w:pPr")
1836        .into_iter()
1837        .next()
1838        .or_else(|| regex_tag(source_xml, r"(?i)<w:pPr\b[^>]*/?>"))
1839        .unwrap_or_default();
1840    re::get(r"(?i)<w:jc\b").is_some_and(|re| re.is_match(&paragraph_properties_xml))
1841}
1842
1843fn apply_paragraph_alignment_to_table_cell_content(
1844    nodes: &mut [TableCellContentNode],
1845    paragraph_align: ParagraphAlignment,
1846) {
1847    for node in nodes.iter_mut() {
1848        match node {
1849            TableCellContentNode::Paragraph(paragraph) => {
1850                if paragraph_has_direct_alignment(paragraph) {
1851                    continue;
1852                }
1853                let style = paragraph.style.get_or_insert(crate::model::ParagraphStyle {
1854                    source_before_auto_spacing: None,
1855                    source_after_auto_spacing: None,
1856                    align: None,
1857                    snap_to_grid: None,
1858                    source_snap_to_grid: None,
1859                    source_has_snap_to_grid: None,
1860                    source_inherited_snap_to_grid: None,
1861                    source_table_snap_to_grid: None,
1862                    text_alignment: None,
1863                    source_text_alignment: None,
1864                    source_has_text_alignment: None,
1865                    source_inherited_text_alignment: None,
1866                    source_table_text_alignment: None,
1867                    heading_level: None,
1868                    style_id: None,
1869                    style_name: None,
1870                    numbering: None,
1871                    spacing: None,
1872                    indent: None,
1873                    background_color: None,
1874                    borders: None,
1875                    tab_stops: None,
1876                    contextual_spacing: None,
1877                    keep_next: None,
1878                    keep_lines: None,
1879                    widow_control: None,
1880                    page_break_before: None,
1881                    drop_cap: None,
1882                }.into());
1883                style.align = Some(paragraph_align);
1884            }
1885            TableCellContentNode::Table(nested_table) => {
1886                for row in &mut nested_table.rows {
1887                    for cell in &mut row.cells {
1888                        apply_paragraph_alignment_to_table_cell_content(
1889                            &mut cell.nodes,
1890                            paragraph_align,
1891                        );
1892                    }
1893                }
1894            }
1895        }
1896    }
1897}
1898
1899fn regex_capture(xml: &str, pattern: &str) -> Option<String> {
1900    re::get(pattern)?
1901        .captures(xml)
1902        .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
1903}
1904
1905fn regex_tag(xml: &str, pattern: &str) -> Option<String> {
1906    re::get(pattern)?.find(xml).map(|m| m.as_str().to_string())
1907}
1908
1909fn regex_find<'a>(xml: &'a str, pattern: &str) -> Option<&'a str> {
1910    re::get(pattern)?.find(xml).map(|m| m.as_str())
1911}