Skip to main content

extend_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    TableNode {
953        r#type: TableNodeType::Table,
954        block_id: None,
955        rows,
956        style: if has_table_style {
957            Some(TableStyle {
958                style_id: table_style_id.clone(),
959                source_style_id: table_style_id,
960                width_twips,
961                source_width: Some(TableWidthSource {
962                    preferred_width: preferred_width.clone(),
963                    width_twips,
964                    inherited_preferred_width: style_table_properties
965                        .as_ref()
966                        .and_then(|properties| properties.preferred_width.clone()),
967                }),
968                preferred_width,
969                alignment,
970                source_alignment: alignment,
971                source_inherited_alignment: style_table_properties
972                    .as_ref()
973                    .and_then(|properties| properties.alignment),
974                bidi_visual,
975                source_bidi_visual: bidi_visual,
976                source_inherited_bidi_visual: style_table_properties
977                    .as_ref()
978                    .and_then(|properties| properties.bidi_visual),
979                indent_twips,
980                layout,
981                cell_spacing_twips,
982                cell_margin_twips,
983                column_widths_twips: if column_widths_twips.is_empty() && table_grid_xml.is_none() {
984                    None
985                } else {
986                    Some(column_widths_twips)
987                },
988                collapsed_grid_columns,
989                borders: resolved_table_borders,
990                floating,
991            })
992        } else {
993            None
994        },
995        source_xml: Some(table_xml.to_string()),
996        source_text_patches: None,
997    }
998}
999
1000/// Some generators emit a placeholder tblGrid while each row's real geometry
1001/// lives in per-cell tcW values (rows may even have differing boundaries).
1002/// Preserve the existing physical reconstruction when preferred widths differ
1003/// from the declared grid, while retaining compatible logical boundaries.
1004fn normalize_conflicting_table_grid(
1005    grid: Vec<i64>,
1006    rows: &mut [TableRowNode],
1007    preserve_collapsed_columns: bool,
1008    resolve_shared_constraints: bool,
1009) -> (Vec<i64>, Option<Vec<usize>>) {
1010    const BOUNDARY_TOLERANCE_TWIPS: i64 = 80;
1011    if grid.is_empty() || rows.is_empty() {
1012        return (grid, None);
1013    }
1014
1015    let span_of = |cell: &TableCellNode| -> usize {
1016        cell.style
1017            .as_ref()
1018            .and_then(|style| style.grid_span)
1019            .unwrap_or(1)
1020            .max(1) as usize
1021    };
1022    let width_of = |cell: &TableCellNode| -> Option<i64> {
1023        cell.style
1024            .as_ref()
1025            .and_then(|style| style.width_twips)
1026            .filter(|&width| width > 0)
1027    };
1028    let is_continuation = |cell: &TableCellNode| -> bool {
1029        cell.style
1030            .as_ref()
1031            .and_then(|style| style.v_merge_continuation)
1032            .unwrap_or(false)
1033    };
1034
1035    // Only regrid when the declared grid is structurally consistent with the
1036    // rows (cell spans actually address its columns) but width-wrong. When the
1037    // grid is structurally bogus (e.g. one gridCol for a three-column table),
1038    // the row-derived fallbacks downstream already model Word's behavior.
1039    let max_span_sum = rows
1040        .iter()
1041        .map(|row| row.cells.iter().map(|cell| span_of(cell)).sum::<usize>())
1042        .max()
1043        .unwrap_or(0);
1044    if max_span_sum != grid.len() {
1045        return (grid, None);
1046    }
1047
1048    // Generators that slice page layouts into (nested) tables emit uniform
1049    // placeholder grids — equal divisions of the table width — while the real
1050    // geometry lives in per-cell tcW values. Small per-cell deviations slip
1051    // under the conflict ratio below yet still misplace the text fragments the
1052    // cells position, so when the declared grid is uniform and every cell
1053    // carries an explicit width, trust the cell widths outright.
1054    let grid_is_uniform = grid.len() > 1 && grid.iter().all(|&width| (width - grid[0]).abs() <= 1);
1055    let cells_fully_measured = rows.iter().all(|row| {
1056        row.cells
1057            .iter()
1058            .all(|cell| is_continuation(cell) || width_of(cell).is_some())
1059    });
1060    let uniform_placeholder_grid = grid_is_uniform && cells_fully_measured;
1061
1062    // Conflict detection: do explicit cell widths disagree with the grid?
1063    let mut measured_rows = 0usize;
1064    let mut conflict_rows = 0usize;
1065    let mut has_width_difference = false;
1066    for row in rows.iter() {
1067        let mut cursor = 0usize;
1068        let mut measured = 0usize;
1069        let mut conflicts = 0usize;
1070        for cell in &row.cells {
1071            let span = span_of(cell);
1072            let expected: i64 = grid.iter().skip(cursor).take(span).sum();
1073            cursor += span;
1074            // Continuation cells inherit the anchor row's geometry, and
1075            // generators often stamp them with placeholder widths copied from
1076            // the bogus grid — they are evidence of nothing.
1077            if is_continuation(cell) {
1078                continue;
1079            }
1080            let Some(actual) = width_of(cell) else {
1081                continue;
1082            };
1083            has_width_difference |= actual != expected;
1084            if expected <= 0 {
1085                continue;
1086            }
1087            measured += 1;
1088            if (actual - expected).abs() * 5 > expected {
1089                conflicts += 1;
1090            }
1091        }
1092        if measured > 0 {
1093            measured_rows += 1;
1094            if conflicts * 2 > measured {
1095                conflict_rows += 1;
1096            }
1097        }
1098    }
1099    // Retain shared columns when the preferred widths already agree. A union
1100    // of cell boundaries can otherwise erase columns inside spanning cells.
1101    if !has_width_difference
1102        || measured_rows == 0
1103        || (!uniform_placeholder_grid && conflict_rows * 2 <= measured_rows)
1104    {
1105        return (grid, None);
1106    }
1107
1108    // Union of every row's cumulative cell boundaries; cells without an
1109    // explicit width fall back to the original grid width for their span.
1110    // Vertically merged cells must share the anchor row's width: HTML rowspan
1111    // cannot express per-row geometry, so continuation cells adopt the
1112    // anchor's width and the difference is absorbed by the cells that follow
1113    // (matching how LibreOffice resolves such tables).
1114    let bucket_of = |position: i64| -> i64 {
1115        (position + BOUNDARY_TOLERANCE_TWIPS / 2) / BOUNDARY_TOLERANCE_TWIPS
1116    };
1117    let mut anchor_width_by_bucket: HashMap<i64, i64> = HashMap::new();
1118    let mut boundaries: Vec<i64> = vec![0];
1119    for row in rows.iter() {
1120        let mut cursor = 0usize;
1121        let mut position = 0i64;
1122        for cell in &row.cells {
1123            let span = span_of(cell);
1124            let fallback: i64 = grid.iter().skip(cursor).take(span).sum();
1125            cursor += span;
1126            let own_width = width_of(cell).unwrap_or(fallback.max(1));
1127            let bucket = bucket_of(position);
1128            let width = if is_continuation(cell) {
1129                *anchor_width_by_bucket.get(&bucket).unwrap_or(&own_width)
1130            } else {
1131                anchor_width_by_bucket.insert(bucket, own_width);
1132                own_width
1133            };
1134            position += width;
1135            boundaries.push(position);
1136        }
1137    }
1138    boundaries.sort_unstable();
1139    let mut merged: Vec<i64> = Vec::new();
1140    for boundary in boundaries {
1141        match merged.last() {
1142            Some(&last) if boundary - last <= BOUNDARY_TOLERANCE_TWIPS => {}
1143            _ => merged.push(boundary),
1144        }
1145    }
1146    if merged.len() < 2 {
1147        return (grid, None);
1148    }
1149
1150    let snap = |value: i64| -> usize {
1151        match merged.binary_search(&value) {
1152            Ok(index) => index,
1153            Err(index) => {
1154                if index == 0 {
1155                    0
1156                } else if index >= merged.len() {
1157                    merged.len() - 1
1158                } else if value - merged[index - 1] <= merged[index] - value {
1159                    index - 1
1160                } else {
1161                    index
1162                }
1163            }
1164        }
1165    };
1166
1167    let mut source_boundaries = vec![None; grid.len() + 1];
1168    let mut source_topology_consistent = true;
1169    let mut resolved_spans = Vec::new();
1170    anchor_width_by_bucket.clear();
1171    for row in rows.iter() {
1172        let mut cursor = 0usize;
1173        let mut position = 0i64;
1174        let mut resolved_cursor = 0usize;
1175        for cell in &row.cells {
1176            let span = span_of(cell);
1177            let source_start = cursor;
1178            let fallback: i64 = grid.iter().skip(cursor).take(span).sum();
1179            cursor += span;
1180            let own_width = width_of(cell).unwrap_or(fallback.max(1));
1181            let bucket = bucket_of(position);
1182            let width = if is_continuation(cell) {
1183                *anchor_width_by_bucket.get(&bucket).unwrap_or(&own_width)
1184            } else {
1185                anchor_width_by_bucket.insert(bucket, own_width);
1186                own_width
1187            };
1188            let start_index = snap(position);
1189            position += width;
1190            let end_index = snap(position).max(start_index + 1);
1191            let new_span = (end_index - start_index) as i64;
1192            resolved_spans.push(new_span);
1193            if start_index != resolved_cursor || end_index >= merged.len() {
1194                source_topology_consistent = false;
1195            } else {
1196                for (column, boundary) in [
1197                    (source_start, merged[start_index]),
1198                    (cursor, merged[end_index]),
1199                ] {
1200                    match source_boundaries.get_mut(column) {
1201                        Some(value @ None) => *value = Some(boundary),
1202                        Some(Some(value)) if *value == boundary => {}
1203                        _ => source_topology_consistent = false,
1204                    }
1205                }
1206            }
1207            resolved_cursor += new_span as usize;
1208        }
1209    }
1210    // Keep logical columns when all rows agree on their resolved boundaries.
1211    // Interior columns may be unobserved because every row spans over them.
1212    if source_topology_consistent {
1213        if let Some(resolved_grid) =
1214            preserve_shared_grid_topology(&grid, &source_boundaries, preserve_collapsed_columns)
1215        {
1216            let collapsed: Vec<_> = resolved_grid
1217                .iter()
1218                .enumerate()
1219                .filter_map(|(column, &width)| (width == 0).then_some(column))
1220                .collect();
1221            if collapsed.is_empty()
1222                || resolved_grid
1223                    .iter()
1224                    .copied()
1225                    .filter(|&width| width > 0)
1226                    .eq(merged.windows(2).map(|pair| pair[1] - pair[0]))
1227            {
1228                return (resolved_grid, (!collapsed.is_empty()).then_some(collapsed));
1229            }
1230        }
1231    }
1232
1233    if !source_topology_consistent && resolve_shared_constraints {
1234        if let Some(widths) = resolve_shared_column_preferences(grid.len(), rows) {
1235            return (widths, None);
1236        }
1237    }
1238
1239    let reduces_merged_span = rows
1240        .iter()
1241        .flat_map(|row| &row.cells)
1242        .zip(&resolved_spans)
1243        .any(|(cell, &resolved_span)| {
1244            let source_span = span_of(cell);
1245            source_span > 1 && resolved_span < source_span as i64
1246        });
1247    if reduces_merged_span {
1248        if let Some(widths) = resolve_repeated_unmerged_grid(&grid, rows) {
1249            return (widths, None);
1250        }
1251    }
1252
1253    let mut resolved_spans = resolved_spans.into_iter();
1254    for row in rows.iter_mut() {
1255        for cell in &mut row.cells {
1256            let new_span = resolved_spans.next().unwrap_or(1);
1257            if new_span > 1 {
1258                let style = cell.style.get_or_insert(TableCellStyle {
1259                    background_color: None,
1260                    grid_span: None,
1261                    row_span: None,
1262                    v_merge_continuation: None,
1263                    width_twips: None,
1264                    preferred_width: None,
1265                    source_width: None,
1266                    margin_twips: None,
1267                    vertical_align: None,
1268                    text_direction: None,
1269                    source_text_direction: None,
1270                    no_wrap: None,
1271                    source_no_wrap: None,
1272                    borders: None,
1273                });
1274                style.grid_span = Some(new_span);
1275            } else if let Some(style) = cell.style.as_mut() {
1276                style.grid_span = None;
1277            }
1278        }
1279    }
1280
1281    (
1282        merged.windows(2).map(|pair| pair[1] - pair[0]).collect(),
1283        None,
1284    )
1285}
1286
1287/// Repeated complete rows can establish shared columns independently of a
1288/// spanning cell's undersized preferred width. Keep their measured widths
1289/// without turning that preference into a shorter logical span.
1290fn resolve_repeated_unmerged_grid(grid: &[i64], rows: &[TableRowNode]) -> Option<Vec<i64>> {
1291    if grid.is_empty() || grid.iter().any(|&width| width <= 0) {
1292        return None;
1293    }
1294    let measured_width = |cell: &TableCellNode| -> Option<i64> {
1295        let style = cell.style.as_ref()?;
1296        if !matches!(
1297            style.preferred_width.as_ref(),
1298            Some(crate::model::TablePreferredWidth::Dxa { .. })
1299        ) {
1300            return None;
1301        }
1302        style.width_twips.filter(|&width| width > 0)
1303    };
1304    let mut anchored_widths: Option<Vec<i64>> = None;
1305    let mut independent_rows = 0usize;
1306    for row in rows {
1307        if row.style.as_ref().is_some_and(|style| {
1308            style.grid_before.unwrap_or(0) != 0 || style.grid_after.unwrap_or(0) != 0
1309        }) {
1310            return None;
1311        }
1312        if row.cells.len() != grid.len() {
1313            continue;
1314        }
1315        let widths = row.cells.iter().map(|cell| {
1316            let style = cell.style.as_ref()?;
1317            if style.grid_span.unwrap_or(1) != 1
1318                || style.row_span.is_some()
1319                || style.v_merge_continuation.unwrap_or(false)
1320            {
1321                return None;
1322            }
1323            measured_width(cell)
1324        });
1325        match &anchored_widths {
1326            Some(anchored) => {
1327                let agrees = widths
1328                    .zip(anchored)
1329                    .try_fold(true, |same, (width, &expected)| {
1330                        width.map(|actual| same && actual == expected)
1331                    });
1332                match agrees {
1333                    Some(true) => {}
1334                    Some(false) => return None,
1335                    None => continue,
1336                }
1337            }
1338            None => {
1339                let Some(widths) = widths.collect() else {
1340                    continue;
1341                };
1342                anchored_widths = Some(widths);
1343            }
1344        }
1345        independent_rows += 1;
1346    }
1347    if independent_rows < 2 {
1348        return None;
1349    }
1350    let anchored_widths = anchored_widths?;
1351    let mut preceding_merges = Vec::new();
1352    let mut next_merges = Vec::new();
1353    for row in rows {
1354        if row.cells.is_empty() {
1355            return None;
1356        }
1357        next_merges.clear();
1358        let mut start = 0usize;
1359        for cell in &row.cells {
1360            let style = cell.style.as_ref()?;
1361            let span = usize::try_from(style.grid_span.unwrap_or(1)).ok()?;
1362            if span == 0 {
1363                return None;
1364            }
1365            let end = start.checked_add(span)?;
1366            let columns = anchored_widths.get(start..end)?;
1367            if style.v_merge_continuation.unwrap_or(false) {
1368                if preceding_merges.binary_search(&(start, end)).is_err() {
1369                    return None;
1370                }
1371                next_merges.push((start, end));
1372            } else {
1373                if style.row_span.is_some_and(|span| span > 0) {
1374                    next_merges.push((start, end));
1375                }
1376                let expected = columns
1377                    .iter()
1378                    .try_fold(0i64, |total, &width| total.checked_add(width))?;
1379                let actual = measured_width(cell)?;
1380                if actual > expected || (span == 1 && actual != expected) {
1381                    return None;
1382                }
1383            }
1384            start = end;
1385        }
1386        if start != grid.len() {
1387            return None;
1388        }
1389        std::mem::swap(&mut preceding_merges, &mut next_merges);
1390    }
1391    Some(anchored_widths)
1392}
1393
1394/// Resolve fully anchored cell preferences on the shared logical columns.
1395fn resolve_shared_column_preferences(columns: usize, rows: &[TableRowNode]) -> Option<Vec<i64>> {
1396    let mut constraints: Vec<Vec<(usize, i64)>> = vec![Vec::new(); columns + 1];
1397    let mut anchored = vec![false; columns];
1398    for row in rows {
1399        let mut start = 0usize;
1400        for cell in &row.cells {
1401            let style = cell.style.as_ref()?;
1402            let span = usize::try_from(style.grid_span.unwrap_or(1)).ok()?;
1403            if span == 0 {
1404                return None;
1405            }
1406            let end = start.checked_add(span)?;
1407            if end > columns {
1408                return None;
1409            }
1410            if !style.v_merge_continuation.unwrap_or(false) {
1411                if !matches!(
1412                    style.preferred_width.as_ref(),
1413                    Some(crate::model::TablePreferredWidth::Dxa { .. })
1414                ) {
1415                    return None;
1416                }
1417                let width = style.width_twips.filter(|&value| value > 0)?;
1418                constraints[end].push((start, width));
1419                if span == 1 {
1420                    anchored[start] = true;
1421                }
1422            }
1423            start = end;
1424        }
1425        if start != columns {
1426            return None;
1427        }
1428    }
1429    if anchored.iter().any(|&value| !value) {
1430        return None;
1431    }
1432    let mut boundaries = vec![0i64; columns + 1];
1433    for end in 1..=columns {
1434        let mut position = boundaries[end - 1];
1435        for &(start, width) in &constraints[end] {
1436            position = position.max(boundaries[start].checked_add(width)?);
1437        }
1438        boundaries[end] = position;
1439    }
1440    Some(
1441        boundaries
1442            .windows(2)
1443            .map(|pair| pair[1] - pair[0])
1444            .collect(),
1445    )
1446}
1447
1448fn preserve_shared_grid_topology(
1449    grid: &[i64],
1450    boundaries: &[Option<i64>],
1451    allow_collapsed: bool,
1452) -> Option<Vec<i64>> {
1453    if boundaries.first().copied().flatten() != Some(0)
1454        || boundaries.last().copied().flatten().is_none()
1455        || grid.iter().any(|&width| width <= 0)
1456    {
1457        return None;
1458    }
1459    let minimum_column_width = if allow_collapsed { 0 } else { 1 };
1460    let mut result = Vec::with_capacity(grid.len());
1461    let mut start = 0usize;
1462    let mut position = 0i64;
1463    for (end, boundary) in boundaries.iter().enumerate().skip(1) {
1464        let Some(boundary) = *boundary else { continue };
1465        let width = boundary.checked_sub(position)?;
1466        if width < minimum_column_width * (end - start) as i64 {
1467            return None;
1468        }
1469        let source_width: i128 = grid[start..end].iter().map(|&value| value as i128).sum();
1470        let mut source_position = 0i128;
1471        let mut resolved_position = 0i64;
1472        for &column_width in &grid[start..end] {
1473            source_position += column_width as i128;
1474            let next_position = (source_position * width as i128 / source_width) as i64;
1475            let resolved_width = next_position - resolved_position;
1476            if resolved_width < minimum_column_width {
1477                return None;
1478            }
1479            result.push(resolved_width);
1480            resolved_position = next_position;
1481        }
1482        start = end;
1483        position = boundary;
1484    }
1485    Some(result)
1486}
1487
1488fn parse_table_look(table_properties_xml: Option<&str>) -> Option<ParsedTableLook> {
1489    let table_properties_xml = table_properties_xml?;
1490    let table_look_tag =
1491        regex_tag(table_properties_xml, r"(?i)<w:tblLook\b[^>]*/?>").unwrap_or_default();
1492    if table_look_tag.is_empty() {
1493        return None;
1494    }
1495    let look_mask_raw = get_attribute(&table_look_tag, "w:val");
1496    let look_mask = look_mask_raw.and_then(|v| i64::from_str_radix(&v, 16).ok());
1497    let row_band_size_tag = regex_tag(
1498        table_properties_xml,
1499        r"(?i)<w:tblStyleRowBandSize\b[^>]*/?>",
1500    )
1501    .or_else(|| {
1502        extract_balanced_tag_blocks(table_properties_xml, "w:tblStyleRowBandSize")
1503            .into_iter()
1504            .next()
1505    });
1506    let col_band_size_tag = regex_tag(
1507        table_properties_xml,
1508        r"(?i)<w:tblStyleColBandSize\b[^>]*/?>",
1509    )
1510    .or_else(|| {
1511        extract_balanced_tag_blocks(table_properties_xml, "w:tblStyleColBandSize")
1512            .into_iter()
1513            .next()
1514    });
1515    let row_band_size_raw = row_band_size_tag
1516        .as_deref()
1517        .and_then(|tag| parse_integer_attribute(tag, "w:val"))
1518        .unwrap_or(1)
1519        .max(1);
1520    let col_band_size_raw = col_band_size_tag
1521        .as_deref()
1522        .and_then(|tag| parse_integer_attribute(tag, "w:val"))
1523        .unwrap_or(1)
1524        .max(1);
1525    let resolve_on_off = |attribute: &str| -> Option<bool> {
1526        let value = get_attribute(&table_look_tag, attribute)?.to_ascii_lowercase();
1527        match value.as_str() {
1528            "1" | "true" | "on" => Some(true),
1529            "0" | "false" | "off" => Some(false),
1530            _ => None,
1531        }
1532    };
1533    let mask_value = look_mask.unwrap_or(0);
1534    Some(ParsedTableLook {
1535        first_row: resolve_on_off("w:firstRow").unwrap_or(mask_value & 0x0020 != 0),
1536        last_row: resolve_on_off("w:lastRow").unwrap_or(mask_value & 0x0040 != 0),
1537        first_col: resolve_on_off("w:firstColumn").unwrap_or(mask_value & 0x0080 != 0),
1538        last_col: resolve_on_off("w:lastColumn").unwrap_or(mask_value & 0x0100 != 0),
1539        no_h_band: resolve_on_off("w:noHBand").unwrap_or(mask_value & 0x0200 != 0),
1540        no_v_band: resolve_on_off("w:noVBand").unwrap_or(mask_value & 0x0400 != 0),
1541        row_band_size: row_band_size_raw,
1542        col_band_size: col_band_size_raw,
1543    })
1544}
1545
1546fn merge_table_look(
1547    direct: Option<ParsedTableLook>,
1548    inherited: Option<&ParsedTableLook>,
1549) -> ParsedTableLook {
1550    let mut merged = default_table_look();
1551    if let Some(inherited) = inherited {
1552        merged = inherited.clone();
1553    }
1554    if let Some(direct) = direct {
1555        merged = direct;
1556    }
1557    merged
1558}
1559
1560pub(crate) fn resolve_table_grid_skip_count(count: Option<i64>, bound: i64) -> i64 {
1561    count
1562        .filter(|value| *value >= 0 && *value <= bound)
1563        .unwrap_or(0)
1564}
1565
1566pub(crate) fn table_grid_column_bound(table: &TableNode) -> i64 {
1567    if let Some(widths) = table
1568        .style
1569        .as_ref()
1570        .and_then(|style| style.column_widths_twips.as_ref())
1571    {
1572        widths.len() as i64
1573    } else {
1574        table
1575            .rows
1576            .iter()
1577            .map(|row| {
1578                row.cells.iter().fold(0i64, |total, cell| {
1579                    total.saturating_add(
1580                        cell.style
1581                            .as_ref()
1582                            .and_then(|style| style.grid_span)
1583                            .unwrap_or(1)
1584                            .max(1),
1585                    )
1586                })
1587            })
1588            .max()
1589            .unwrap_or(0)
1590    }
1591}
1592
1593pub(crate) fn resolve_table_condition_for_cell(
1594    table_style: &ParsedTableStyleDefinition,
1595    table_look: &ParsedTableLook,
1596    row_index: i64,
1597    row_count: i64,
1598    start_column_index: i64,
1599    end_column_index: i64,
1600    column_count: i64,
1601) -> Option<ParsedTableStyleCondition> {
1602    let mut condition_types = vec![TableConditionalStyleType::WholeTable];
1603    let is_first_row = row_index == 0;
1604    let is_last_row = row_index == row_count - 1;
1605    let is_first_column = start_column_index == 0;
1606    let is_last_column = end_column_index >= column_count - 1;
1607    let row_band_size = table_look.row_band_size.max(1);
1608    let col_band_size = table_look.col_band_size.max(1);
1609    if !table_look.no_h_band {
1610        let band_row_index = row_index - if table_look.first_row { 1 } else { 0 };
1611        if band_row_index >= 0 {
1612            let band_row_group = band_row_index / row_band_size;
1613            condition_types.push(if band_row_group % 2 == 0 {
1614                TableConditionalStyleType::Band1Horz
1615            } else {
1616                TableConditionalStyleType::Band2Horz
1617            });
1618        }
1619    }
1620    if !table_look.no_v_band {
1621        let band_column_index = start_column_index - if table_look.first_col { 1 } else { 0 };
1622        if band_column_index >= 0 {
1623            let band_column_group = band_column_index / col_band_size;
1624            condition_types.push(if band_column_group % 2 == 0 {
1625                TableConditionalStyleType::Band1Vert
1626            } else {
1627                TableConditionalStyleType::Band2Vert
1628            });
1629        }
1630    }
1631    if table_look.first_row && is_first_row {
1632        condition_types.push(TableConditionalStyleType::FirstRow);
1633    }
1634    if table_look.last_row && is_last_row {
1635        condition_types.push(TableConditionalStyleType::LastRow);
1636    }
1637    if table_look.first_col && is_first_column {
1638        condition_types.push(TableConditionalStyleType::FirstCol);
1639    }
1640    if table_look.last_col && is_last_column {
1641        condition_types.push(TableConditionalStyleType::LastCol);
1642    }
1643    if table_look.first_row && table_look.first_col && is_first_row && is_first_column {
1644        condition_types.push(TableConditionalStyleType::NwCell);
1645    }
1646    if table_look.first_row && table_look.last_col && is_first_row && is_last_column {
1647        condition_types.push(TableConditionalStyleType::NeCell);
1648    }
1649    if table_look.last_row && table_look.first_col && is_last_row && is_first_column {
1650        condition_types.push(TableConditionalStyleType::SwCell);
1651    }
1652    if table_look.last_row && table_look.last_col && is_last_row && is_last_column {
1653        condition_types.push(TableConditionalStyleType::SeCell);
1654    }
1655    let mut resolved_condition: Option<ParsedTableStyleCondition> = None;
1656    for condition_type in condition_types {
1657        resolved_condition = merge_table_conditional_style(
1658            resolved_condition.as_ref(),
1659            table_style.conditions.get(&condition_type),
1660        );
1661    }
1662    resolved_condition
1663}
1664
1665fn merge_table_conditional_style(
1666    inherited: Option<&ParsedTableStyleCondition>,
1667    direct: Option<&ParsedTableStyleCondition>,
1668) -> Option<ParsedTableStyleCondition> {
1669    if inherited.is_none() && direct.is_none() {
1670        return None;
1671    }
1672    let merged = ParsedTableStyleCondition {
1673        row_background_color: direct
1674            .and_then(|d| d.row_background_color.clone())
1675            .or_else(|| inherited.and_then(|i| i.row_background_color.clone())),
1676        cell_background_color: direct
1677            .and_then(|d| d.cell_background_color.clone())
1678            .or_else(|| inherited.and_then(|i| i.cell_background_color.clone())),
1679        cell_preferred_width: direct
1680            .and_then(|value| value.cell_preferred_width.clone())
1681            .or_else(|| inherited.and_then(|value| value.cell_preferred_width.clone())),
1682        paragraph_align: direct
1683            .and_then(|d| d.paragraph_align)
1684            .or(inherited.and_then(|i| i.paragraph_align)),
1685        paragraph_text_alignment: direct
1686            .and_then(|value| value.paragraph_text_alignment)
1687            .or_else(|| inherited.and_then(|value| value.paragraph_text_alignment)),
1688        paragraph_snap_to_grid: direct
1689            .and_then(|value| value.paragraph_snap_to_grid)
1690            .or_else(|| inherited.and_then(|value| value.paragraph_snap_to_grid)),
1691        run_style: merge_text_styles(&[
1692            inherited.and_then(|i| i.run_style.clone()),
1693            direct.and_then(|d| d.run_style.clone()),
1694        ]),
1695        table_borders: merge_table_border_sets(
1696            inherited.and_then(|i| i.table_borders.as_ref()),
1697            direct.and_then(|d| d.table_borders.as_ref()),
1698        ),
1699        cell_borders: merge_table_border_sets(
1700            inherited.and_then(|i| i.cell_borders.as_ref()),
1701            direct.and_then(|d| d.cell_borders.as_ref()),
1702        ),
1703        table_properties: None,
1704        table_look: None,
1705    };
1706    if merged.row_background_color.is_none()
1707        && merged.cell_background_color.is_none()
1708        && merged.cell_preferred_width.is_none()
1709        && merged.paragraph_align.is_none()
1710        && merged.paragraph_text_alignment.is_none()
1711        && merged.paragraph_snap_to_grid.is_none()
1712        && merged.run_style.is_none()
1713        && merged.table_borders.is_none()
1714        && merged.cell_borders.is_none()
1715    {
1716        return None;
1717    }
1718    Some(merged)
1719}
1720
1721fn apply_snap_to_grid_to_table_cell_content(
1722    nodes: &mut [TableCellContentNode],
1723    table_snap_to_grid: bool,
1724    context: &crate::parse::context::ParseContext<
1725        '_,
1726        impl std::borrow::Borrow<super::context::ParsedStyleSheet>,
1727    >,
1728) {
1729    for node in nodes {
1730        let TableCellContentNode::Paragraph(paragraph) = node else {
1731            continue;
1732        };
1733        let properties = paragraph
1734            .source_xml
1735            .as_deref()
1736            .and_then(|xml| direct_table_property(xml, "w:pPr"))
1737            .unwrap_or_default();
1738        let direct_property = direct_table_property(properties, "w:snapToGrid");
1739        let direct =
1740            direct_property.and_then(|tag| crate::xml::parse_on_off_attribute(tag, "snapToGrid"));
1741        let explicit_style = direct_table_property(properties, "w:pStyle")
1742            .and_then(|tag| get_attribute(tag, "w:val"))
1743            .and_then(|id| context.styles().paragraph_style_by_id.get(&id))
1744            .filter(|style| style.source_has_snap_to_grid != Some(false))
1745            .and_then(|style| style.snap_to_grid);
1746        let inherited = explicit_style.or(Some(table_snap_to_grid));
1747        let style = paragraph.style.get_or_insert_with(Default::default);
1748        style.snap_to_grid = direct.or(inherited);
1749        style.source_snap_to_grid = style.snap_to_grid;
1750        style.source_has_snap_to_grid = Some(direct_property.is_some());
1751        style.source_inherited_snap_to_grid = inherited;
1752        style.source_table_snap_to_grid = Some(table_snap_to_grid);
1753    }
1754}
1755
1756fn apply_text_alignment_to_table_cell_content(
1757    nodes: &mut [TableCellContentNode],
1758    table_alignment: crate::model::ParagraphTextAlignment,
1759    context: &crate::parse::context::ParseContext<
1760        '_,
1761        impl std::borrow::Borrow<super::context::ParsedStyleSheet>,
1762    >,
1763) {
1764    for node in nodes {
1765        let TableCellContentNode::Paragraph(paragraph) = node else {
1766            continue;
1767        };
1768        let properties = paragraph
1769            .source_xml
1770            .as_deref()
1771            .and_then(|xml| direct_table_property(xml, "w:pPr"))
1772            .unwrap_or_default();
1773        let direct = super::style::parse_paragraph_text_alignment_from_xml(properties);
1774        let explicit_style = direct_table_property(properties, "w:pStyle")
1775            .and_then(|tag| get_attribute(tag, "w:val"))
1776            .and_then(|id| context.styles().paragraph_style_by_id.get(&id))
1777            .filter(|style| style.source_has_text_alignment != Some(false))
1778            .and_then(|style| style.text_alignment);
1779        let inherited = explicit_style.or(Some(table_alignment));
1780        let style = paragraph.style.get_or_insert(crate::model::ParagraphStyle {
1781            source_before_auto_spacing: None,
1782            source_after_auto_spacing: None,
1783            align: None,
1784            snap_to_grid: None,
1785            source_snap_to_grid: None,
1786            source_has_snap_to_grid: None,
1787            source_inherited_snap_to_grid: None,
1788            source_table_snap_to_grid: None,
1789            text_alignment: None,
1790            source_text_alignment: None,
1791            source_has_text_alignment: None,
1792            source_inherited_text_alignment: None,
1793            source_table_text_alignment: None,
1794            heading_level: None,
1795            style_id: None,
1796            style_name: None,
1797            numbering: None,
1798            spacing: None,
1799            indent: None,
1800            background_color: None,
1801            borders: None,
1802            tab_stops: None,
1803            contextual_spacing: None,
1804            keep_next: None,
1805            keep_lines: None,
1806            widow_control: None,
1807            page_break_before: None,
1808            drop_cap: None,
1809        }.into());
1810        style.text_alignment = direct.or(inherited);
1811        style.source_has_text_alignment =
1812            Some(direct_table_property(properties, "w:textAlignment").is_some());
1813        style.source_text_alignment = style.text_alignment;
1814        style.source_inherited_text_alignment = inherited;
1815        style.source_table_text_alignment = Some(table_alignment);
1816    }
1817}
1818
1819fn paragraph_has_direct_alignment(paragraph: &crate::model::ParagraphNode) -> bool {
1820    let Some(source_xml) = paragraph.source_xml.as_deref() else {
1821        return false;
1822    };
1823    let paragraph_properties_xml = extract_balanced_tag_blocks(source_xml, "w:pPr")
1824        .into_iter()
1825        .next()
1826        .or_else(|| regex_tag(source_xml, r"(?i)<w:pPr\b[^>]*/?>"))
1827        .unwrap_or_default();
1828    re::get(r"(?i)<w:jc\b").is_some_and(|re| re.is_match(&paragraph_properties_xml))
1829}
1830
1831fn apply_paragraph_alignment_to_table_cell_content(
1832    nodes: &mut [TableCellContentNode],
1833    paragraph_align: ParagraphAlignment,
1834) {
1835    for node in nodes.iter_mut() {
1836        match node {
1837            TableCellContentNode::Paragraph(paragraph) => {
1838                if paragraph_has_direct_alignment(paragraph) {
1839                    continue;
1840                }
1841                let style = paragraph.style.get_or_insert(crate::model::ParagraphStyle {
1842                    source_before_auto_spacing: None,
1843                    source_after_auto_spacing: None,
1844                    align: None,
1845                    snap_to_grid: None,
1846                    source_snap_to_grid: None,
1847                    source_has_snap_to_grid: None,
1848                    source_inherited_snap_to_grid: None,
1849                    source_table_snap_to_grid: None,
1850                    text_alignment: None,
1851                    source_text_alignment: None,
1852                    source_has_text_alignment: None,
1853                    source_inherited_text_alignment: None,
1854                    source_table_text_alignment: None,
1855                    heading_level: None,
1856                    style_id: None,
1857                    style_name: None,
1858                    numbering: None,
1859                    spacing: None,
1860                    indent: None,
1861                    background_color: None,
1862                    borders: None,
1863                    tab_stops: None,
1864                    contextual_spacing: None,
1865                    keep_next: None,
1866                    keep_lines: None,
1867                    widow_control: None,
1868                    page_break_before: None,
1869                    drop_cap: None,
1870                }.into());
1871                style.align = Some(paragraph_align);
1872            }
1873            TableCellContentNode::Table(nested_table) => {
1874                for row in &mut nested_table.rows {
1875                    for cell in &mut row.cells {
1876                        apply_paragraph_alignment_to_table_cell_content(
1877                            &mut cell.nodes,
1878                            paragraph_align,
1879                        );
1880                    }
1881                }
1882            }
1883        }
1884    }
1885}
1886
1887fn regex_capture(xml: &str, pattern: &str) -> Option<String> {
1888    re::get(pattern)?
1889        .captures(xml)
1890        .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
1891}
1892
1893fn regex_tag(xml: &str, pattern: &str) -> Option<String> {
1894    re::get(pattern)?.find(xml).map(|m| m.as_str().to_string())
1895}
1896
1897fn regex_find<'a>(xml: &'a str, pattern: &str) -> Option<&'a str> {
1898    re::get(pattern)?.find(xml).map(|m| m.as_str())
1899}