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 {
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
1000fn 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 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 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 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 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 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 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 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
1287fn 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
1394fn 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(¶graph_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}