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