1use std::collections::{HashMap, HashSet};
2
3use super::colors::normalize_hex_color;
4use super::context::{
5 empty_style_sheet, ParsedStyleSheet, ParsedTableLook, ParsedTableProperties,
6 ParsedTableStyleCondition, ParsedTableStyleDefinition, TableConditionalStyleType,
7 ThemeColorMap, ThemeFontMap,
8};
9use crate::model::NumberingDefinitionSet;
10use crate::model::{
11 HeadingLevel, ImageHorizontalAlign, ImageVerticalAlign, ParagraphAlignment, ParagraphBorderSet,
12 ParagraphBorderStyle, ParagraphIndent, ParagraphLineRule, ParagraphNumbering, ParagraphSpacing,
13 ParagraphStyle, ParagraphStyleDefinition, ParagraphTabStop, ParagraphTabStopAlignment,
14 ParagraphTabStopLeader, ParagraphTextAlignment, TableAlignment, TableBorderSet,
15 TableBorderStyle, TableBoxSpacing, TableFloating, TableLayout, TablePreferredWidth,
16 TextRunBorderStyle, TextStyle, VerticalAlign,
17};
18use crate::package::OoxmlPackage;
19use crate::xml::{
20 decode_xml_entities, get_attribute, normalize_alignment, parse_integer_attribute,
21 parse_on_off_attribute, parse_underline_attribute,
22};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25enum RawStyleType {
26 Paragraph,
27 Character,
28 Numbering,
29 Table,
30}
31
32struct RawStyleDefinition {
33 id: String,
34 style_type: RawStyleType,
35 name: String,
36 based_on_id: Option<String>,
37 next_style_id: Option<String>,
38 align: Option<ParagraphAlignment>,
39 snap_to_grid: Option<bool>,
40 text_alignment: Option<ParagraphTextAlignment>,
41 heading_level: Option<HeadingLevel>,
42 outline_heading: Option<Option<HeadingLevel>>,
43 numbering: Option<ParagraphNumbering>,
44 spacing: Option<ParagraphSpacing>,
45 indent: Option<ParagraphIndent>,
46 background_color: Option<String>,
47 borders: Option<ParagraphBorderSet>,
48 tab_stops: Option<Vec<ParagraphTabStop>>,
49 contextual_spacing: Option<bool>,
50 keep_next: Option<bool>,
51 keep_lines: Option<bool>,
52 widow_control: Option<bool>,
53 page_break_before: Option<bool>,
54 run_style: Option<TextStyle>,
55 ui_priority: Option<i64>,
56 is_default: Option<bool>,
57 is_primary: Option<bool>,
58}
59
60const DEFAULT_TABLE_LOOK: ParsedTableLook = ParsedTableLook {
61 first_row: false,
62 last_row: false,
63 first_col: false,
64 last_col: false,
65 no_h_band: true,
66 no_v_band: true,
67 row_band_size: 1,
68 col_band_size: 1,
69};
70
71fn map_alignment(value: Option<&str>) -> Option<ParagraphAlignment> {
72 match normalize_alignment(value)? {
73 "left" => Some(ParagraphAlignment::Left),
74 "center" => Some(ParagraphAlignment::Center),
75 "right" => Some(ParagraphAlignment::Right),
76 "justify" => Some(ParagraphAlignment::Justify),
77 _ => None,
78 }
79}
80
81pub fn normalize_heading_level(value: Option<&str>) -> Option<HeadingLevel> {
83 let value = value?;
84 let lower = value.to_ascii_lowercase();
85 let bytes = lower.as_bytes();
86 let needle = b"heading";
87 let mut index = 0;
88
89 while index + needle.len() <= bytes.len() {
90 if bytes[index..index + needle.len()]
91 .iter()
92 .zip(needle.iter())
93 .all(|(left, right)| left.eq_ignore_ascii_case(right))
94 {
95 let mut cursor = index + needle.len();
96 while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
97 cursor += 1;
98 }
99 if let Some(digit) = bytes
100 .get(cursor)
101 .and_then(|byte| (*byte as char).to_digit(10))
102 {
103 if (1..=9).contains(&digit) {
104 return match digit {
105 1 => Some(HeadingLevel::One),
106 2 => Some(HeadingLevel::Two),
107 3 => Some(HeadingLevel::Three),
108 4 => Some(HeadingLevel::Four),
109 5 => Some(HeadingLevel::Five),
110 6 => Some(HeadingLevel::Six),
111 7 => Some(HeadingLevel::Seven),
112 8 => Some(HeadingLevel::Eight),
113 9 => Some(HeadingLevel::Nine),
114 _ => None,
115 };
116 }
117 }
118 }
119 index += 1;
120 }
121
122 None
123}
124
125pub fn parse_paragraph_numbering_from_xml(xml: &str) -> Option<ParagraphNumbering> {
127 if xml.is_empty() {
128 return None;
129 }
130
131 let numbering_xml = crate::xml::extract_balanced_tag_slices(xml, "w:numPr")
132 .into_iter()
133 .next()
134 .or_else(|| super::scan::find_tag_token_ref(xml, "w:numPr"))
135 .unwrap_or_default();
136 if numbering_xml.is_empty() {
137 return None;
138 }
139
140 let num_id_raw = find_namespaced_integer_attribute(&numbering_xml, "w:numId", "w:val");
141 let num_id = num_id_raw?;
142 if num_id <= 0 {
143 return None;
144 }
145
146 let ilvl_raw = find_namespaced_integer_attribute(&numbering_xml, "w:ilvl", "w:val");
147 let ilvl_value = ilvl_raw.unwrap_or(0);
148
149 Some(ParagraphNumbering {
150 num_id,
151 ilvl: if ilvl_value >= 0 { ilvl_value } else { 0 },
152 })
153}
154
155fn find_namespaced_integer_attribute(xml: &str, tag_name: &str, attribute: &str) -> Option<i64> {
156 let tag = super::scan::find_tag_token_ref(xml, tag_name)?;
157 parse_integer_attribute(&tag, attribute)
158}
159
160pub fn parse_paragraph_spacing_from_xml(xml: &str) -> Option<ParagraphSpacing> {
162 if xml.is_empty() {
163 return None;
164 }
165
166 let spacing_tag = super::scan::find_tag_token_ref(xml, "w:spacing")?;
167 let line_rule_raw =
168 get_attribute(&spacing_tag, "w:lineRule").map(|value| value.to_ascii_lowercase());
169 let line_rule = match line_rule_raw.as_deref() {
170 Some("auto") => Some(ParagraphLineRule::Auto),
171 Some("exact") => Some(ParagraphLineRule::Exact),
172 Some("atleast") => Some(ParagraphLineRule::AtLeast),
173 _ => None,
174 };
175
176 let spacing = ParagraphSpacing {
177 before_twips: parse_integer_attribute(&spacing_tag, "w:before"),
178 after_twips: parse_integer_attribute(&spacing_tag, "w:after"),
179 before_auto_spacing: parse_on_off_value(
180 get_attribute(&spacing_tag, "w:beforeAutospacing").as_deref(),
181 ),
182 after_auto_spacing: parse_on_off_value(
183 get_attribute(&spacing_tag, "w:afterAutospacing").as_deref(),
184 ),
185 line_twips: parse_integer_attribute(&spacing_tag, "w:line"),
186 line_rule,
187 };
188
189 if spacing.before_twips.is_none()
190 && spacing.after_twips.is_none()
191 && spacing.before_auto_spacing.is_none()
192 && spacing.after_auto_spacing.is_none()
193 && spacing.line_twips.is_none()
194 && spacing.line_rule.is_none()
195 {
196 return None;
197 }
198
199 Some(spacing)
200}
201
202pub fn parse_paragraph_indent_from_xml(xml: &str) -> Option<ParagraphIndent> {
204 if xml.is_empty() {
205 return None;
206 }
207
208 let indent_tag = super::scan::find_tag_token_ref(xml, "w:ind")?;
209 let indent = ParagraphIndent {
210 left_twips: parse_integer_attribute(&indent_tag, "w:left"),
211 right_twips: parse_integer_attribute(&indent_tag, "w:right"),
212 first_line_twips: parse_integer_attribute(&indent_tag, "w:firstLine"),
213 hanging_twips: parse_integer_attribute(&indent_tag, "w:hanging"),
214 };
215
216 if indent.left_twips.is_none()
217 && indent.right_twips.is_none()
218 && indent.first_line_twips.is_none()
219 && indent.hanging_twips.is_none()
220 {
221 return None;
222 }
223
224 Some(indent)
225}
226
227fn normalize_tab_stop_alignment(value: Option<&str>) -> Option<ParagraphTabStopAlignment> {
228 match value?.to_ascii_lowercase().as_str() {
229 "clear" => Some(ParagraphTabStopAlignment::Clear),
230 "left" => Some(ParagraphTabStopAlignment::Left),
231 "center" => Some(ParagraphTabStopAlignment::Center),
232 "right" => Some(ParagraphTabStopAlignment::Right),
233 "decimal" => Some(ParagraphTabStopAlignment::Decimal),
234 "bar" => Some(ParagraphTabStopAlignment::Bar),
235 _ => None,
236 }
237}
238
239fn normalize_tab_stop_leader(value: Option<&str>) -> Option<ParagraphTabStopLeader> {
240 match value?.to_ascii_lowercase().as_str() {
241 "none" => Some(ParagraphTabStopLeader::LeaderNone),
242 "dot" => Some(ParagraphTabStopLeader::Dot),
243 "hyphen" => Some(ParagraphTabStopLeader::Hyphen),
244 "underscore" => Some(ParagraphTabStopLeader::Underscore),
245 "middledot" => Some(ParagraphTabStopLeader::MiddleDot),
246 _ => None,
247 }
248}
249
250pub fn parse_paragraph_tab_stops_from_xml(xml: &str) -> Vec<ParagraphTabStop> {
252 if xml.is_empty() {
253 return Vec::new();
254 }
255
256 let tabs_xml = crate::xml::extract_balanced_tag_slices(xml, "w:tabs")
257 .into_iter()
258 .next()
259 .or_else(|| super::scan::find_tag_token_ref(xml, "w:tabs"))
260 .unwrap_or_default();
261 if tabs_xml.is_empty() {
262 return Vec::new();
263 }
264
265 let mut tab_stops = Vec::new();
266 for tab_tag in super::scan::find_all_tag_tokens(&tabs_xml, "w:tab") {
267 let alignment = normalize_tab_stop_alignment(get_attribute(&tab_tag, "w:val").as_deref());
268 let leader = normalize_tab_stop_leader(get_attribute(&tab_tag, "w:leader").as_deref());
269 let Some(position_twips) = parse_integer_attribute(&tab_tag, "w:pos") else {
270 continue;
271 };
272 tab_stops.push(ParagraphTabStop {
273 alignment: alignment.or(Some(ParagraphTabStopAlignment::Left)),
274 leader: leader.or(Some(ParagraphTabStopLeader::LeaderNone)),
275 position_twips: Some(position_twips),
276 });
277 }
278
279 tab_stops.sort_by_key(|stop| stop.position_twips.unwrap_or(0));
280 tab_stops
281}
282
283pub fn parse_paragraph_shading_from_xml(xml: &str) -> Option<String> {
285 if xml.is_empty() {
286 return None;
287 }
288 let shading_tag = super::scan::find_tag_token_ref(xml, "w:shd")?;
289 normalize_hex_color(get_attribute(&shading_tag, "w:fill").as_deref())
290}
291
292pub(super) fn parse_on_off_value(value: Option<&str>) -> Option<bool> {
293 let value = value?;
294 let normalized = value.trim().to_ascii_lowercase();
295 Some(normalized != "0" && normalized != "false" && normalized != "none" && normalized != "off")
296}
297
298pub fn parse_paragraph_border_style(tag_xml: Option<&str>) -> Option<ParagraphBorderStyle> {
300 let tag_xml = tag_xml?;
301 let border_type = get_attribute(tag_xml, "w:val")?.trim().to_ascii_lowercase();
302 if border_type.is_empty() {
303 return None;
304 }
305
306 let size_eighth_pt = parse_integer_attribute(tag_xml, "w:sz");
307 let space_pt = parse_integer_attribute(tag_xml, "w:space");
308 let raw_color = get_attribute(tag_xml, "w:color");
309 let color = if raw_color
310 .as_deref()
311 .is_some_and(|value| value.trim().eq_ignore_ascii_case("auto"))
312 {
313 Some("#000000".to_string())
314 } else {
315 normalize_hex_color(raw_color.as_deref())
316 };
317
318 Some(ParagraphBorderStyle {
319 border_type,
320 size_eighth_pt: size_eighth_pt
321 .filter(|value| *value >= 0)
322 .map(|value| value as f64),
323 space_pt: space_pt
324 .filter(|value| *value >= 0)
325 .map(|value| value as f64),
326 color,
327 frame: parse_on_off_value(get_attribute(tag_xml, "w:frame").as_deref()),
328 shadow: parse_on_off_value(get_attribute(tag_xml, "w:shadow").as_deref()),
329 })
330}
331
332pub fn parse_text_run_border_style(tag_xml: Option<&str>) -> Option<TextRunBorderStyle> {
334 let tag_xml = tag_xml?;
335 let border_type = get_attribute(tag_xml, "w:val")?.trim().to_ascii_lowercase();
336 if border_type.is_empty() {
337 return None;
338 }
339
340 let size_eighth_pt = parse_integer_attribute(tag_xml, "w:sz");
341 let space_pt = parse_integer_attribute(tag_xml, "w:space");
342 let raw_color = get_attribute(tag_xml, "w:color");
343 let color = if raw_color
344 .as_deref()
345 .is_some_and(|value| value.trim().eq_ignore_ascii_case("auto"))
346 {
347 None
348 } else {
349 normalize_hex_color(raw_color.as_deref())
350 };
351
352 Some(TextRunBorderStyle {
353 border_type,
354 size_eighth_pt: size_eighth_pt
355 .filter(|value| *value >= 0)
356 .map(|value| value as f64),
357 space_pt: space_pt
358 .filter(|value| *value >= 0)
359 .map(|value| value as f64),
360 color,
361 frame: parse_on_off_value(get_attribute(tag_xml, "w:frame").as_deref()),
362 shadow: parse_on_off_value(get_attribute(tag_xml, "w:shadow").as_deref()),
363 })
364}
365
366pub fn parse_paragraph_border_set_from_xml(xml: &str) -> Option<ParagraphBorderSet> {
368 if xml.is_empty() {
369 return None;
370 }
371
372 let paragraph_border_xml = crate::xml::extract_balanced_tag_slices(xml, "w:pBdr")
373 .into_iter()
374 .next()
375 .or_else(|| super::scan::find_tag_token_ref(xml, "w:pBdr"))
376 .unwrap_or_default();
377 if paragraph_border_xml.is_empty() {
378 return None;
379 }
380
381 let top = parse_paragraph_border_style(
382 super::scan::find_tag_token_ref(¶graph_border_xml, "w:top").as_deref(),
383 );
384 let right = parse_paragraph_border_style(
385 super::scan::find_tag_token_ref(¶graph_border_xml, "w:right").as_deref(),
386 );
387 let bottom = parse_paragraph_border_style(
388 super::scan::find_tag_token_ref(¶graph_border_xml, "w:bottom").as_deref(),
389 );
390 let left = parse_paragraph_border_style(
391 super::scan::find_tag_token_ref(¶graph_border_xml, "w:left").as_deref(),
392 );
393 let between = parse_paragraph_border_style(
394 super::scan::find_tag_token_ref(¶graph_border_xml, "w:between").as_deref(),
395 );
396 let bar = parse_paragraph_border_style(
397 super::scan::find_tag_token_ref(¶graph_border_xml, "w:bar").as_deref(),
398 );
399
400 if top.is_none()
401 && right.is_none()
402 && bottom.is_none()
403 && left.is_none()
404 && between.is_none()
405 && bar.is_none()
406 {
407 return None;
408 }
409
410 Some(ParagraphBorderSet {
411 top,
412 right,
413 bottom,
414 left,
415 between,
416 bar,
417 })
418}
419
420pub fn parse_table_box_spacing(xml: &str) -> Option<TableBoxSpacing> {
422 let top_match = super::scan::find_tag_token_ref(xml, "w:top");
423 let right_match = super::scan::find_tag_token_ref(xml, "w:right")
426 .or_else(|| super::scan::find_tag_token_ref(xml, "w:end"));
427 let bottom_match = super::scan::find_tag_token_ref(xml, "w:bottom");
428 let left_match = super::scan::find_tag_token_ref(xml, "w:left")
429 .or_else(|| super::scan::find_tag_token_ref(xml, "w:start"));
430
431 let spacing = TableBoxSpacing {
432 top_twips: top_match
433 .as_deref()
434 .and_then(|tag| parse_integer_attribute(tag, "w:w")),
435 right_twips: right_match
436 .as_deref()
437 .and_then(|tag| parse_integer_attribute(tag, "w:w")),
438 bottom_twips: bottom_match
439 .as_deref()
440 .and_then(|tag| parse_integer_attribute(tag, "w:w")),
441 left_twips: left_match
442 .as_deref()
443 .and_then(|tag| parse_integer_attribute(tag, "w:w")),
444 };
445
446 if spacing.top_twips.is_none()
447 && spacing.right_twips.is_none()
448 && spacing.bottom_twips.is_none()
449 && spacing.left_twips.is_none()
450 {
451 return None;
452 }
453
454 Some(spacing)
455}
456
457pub fn parse_table_border_style(tag_xml: Option<&str>) -> Option<TableBorderStyle> {
459 let tag_xml = tag_xml?;
460 let border_type = get_attribute(tag_xml, "w:val")?.trim().to_ascii_lowercase();
461 if border_type.is_empty() {
462 return None;
463 }
464
465 let size_eighth_pt = parse_integer_attribute(tag_xml, "w:sz");
466 let raw_color = get_attribute(tag_xml, "w:color");
467 let color = if raw_color
468 .as_deref()
469 .is_some_and(|value| value.trim().eq_ignore_ascii_case("auto"))
470 {
471 Some("#000000".to_string())
472 } else {
473 normalize_hex_color(raw_color.as_deref())
474 };
475
476 Some(TableBorderStyle {
477 border_type,
478 size_eighth_pt: size_eighth_pt
479 .filter(|value| *value >= 0)
480 .map(|value| value as f64),
481 color,
482 })
483}
484
485pub fn parse_table_border_set(xml: &str) -> Option<TableBorderSet> {
487 let top = parse_table_border_style(super::scan::find_tag_token_ref(xml, "w:top").as_deref());
488 let right =
489 parse_table_border_style(super::scan::find_tag_token_ref(xml, "w:right").as_deref());
490 let bottom =
491 parse_table_border_style(super::scan::find_tag_token_ref(xml, "w:bottom").as_deref());
492 let left = parse_table_border_style(super::scan::find_tag_token_ref(xml, "w:left").as_deref());
493 let inside_h =
494 parse_table_border_style(super::scan::find_tag_token_ref(xml, "w:insideH").as_deref());
495 let inside_v =
496 parse_table_border_style(super::scan::find_tag_token_ref(xml, "w:insideV").as_deref());
497 let tl2br =
498 parse_table_border_style(super::scan::find_tag_token_ref(xml, "w:tl2br").as_deref());
499 let tr2bl =
500 parse_table_border_style(super::scan::find_tag_token_ref(xml, "w:tr2bl").as_deref());
501
502 if top.is_none()
503 && right.is_none()
504 && bottom.is_none()
505 && left.is_none()
506 && inside_h.is_none()
507 && inside_v.is_none()
508 && tl2br.is_none()
509 && tr2bl.is_none()
510 {
511 return None;
512 }
513
514 Some(TableBorderSet {
515 top,
516 right,
517 bottom,
518 left,
519 inside_h,
520 inside_v,
521 tl2br,
522 tr2bl,
523 })
524}
525
526fn has_table_properties(properties: &ParsedTableProperties) -> bool {
527 properties.width_twips.is_some()
528 || properties.preferred_width.is_some()
529 || properties.alignment.is_some()
530 || properties.bidi_visual.is_some()
531 || properties.indent_twips.is_some()
532 || properties.layout.is_some()
533 || properties.cell_spacing_twips.is_some()
534 || properties.floating.is_some()
535 || properties.cell_margin_twips.is_some()
536}
537
538pub fn parse_floating_table_style(table_properties_xml: Option<&str>) -> Option<TableFloating> {
540 let table_properties_xml = table_properties_xml?;
541 let floating_tag = super::scan::find_tag_token_ref(table_properties_xml, "w:tblpPr")?;
542
543 let x_twips = parse_integer_attribute(&floating_tag, "w:tblpX");
544 let y_twips = parse_integer_attribute(&floating_tag, "w:tblpY");
545 let left_from_text_twips = parse_integer_attribute(&floating_tag, "w:leftFromText");
546 let right_from_text_twips = parse_integer_attribute(&floating_tag, "w:rightFromText");
547 let top_from_text_twips = parse_integer_attribute(&floating_tag, "w:topFromText");
548 let bottom_from_text_twips = parse_integer_attribute(&floating_tag, "w:bottomFromText");
549 let horizontal_anchor = get_attribute(&floating_tag, "w:horzAnchor");
550 let vertical_anchor = get_attribute(&floating_tag, "w:vertAnchor");
551 let horizontal_align_raw =
552 get_attribute(&floating_tag, "w:tblpXSpec").map(|value| value.trim().to_ascii_lowercase());
553 let vertical_align_raw =
554 get_attribute(&floating_tag, "w:tblpYSpec").map(|value| value.trim().to_ascii_lowercase());
555
556 let horizontal_align = match horizontal_align_raw.as_deref() {
557 Some("left") => Some(ImageHorizontalAlign::Left),
558 Some("center") => Some(ImageHorizontalAlign::Center),
559 Some("right") => Some(ImageHorizontalAlign::Right),
560 Some("inside") => Some(ImageHorizontalAlign::Inside),
561 Some("outside") => Some(ImageHorizontalAlign::Outside),
562 _ => None,
563 };
564 let vertical_align = match vertical_align_raw.as_deref() {
565 Some("top") => Some(ImageVerticalAlign::Top),
566 Some("center") => Some(ImageVerticalAlign::Center),
567 Some("bottom") => Some(ImageVerticalAlign::Bottom),
568 Some("inside") => Some(ImageVerticalAlign::Inside),
569 Some("outside") => Some(ImageVerticalAlign::Outside),
570 _ => None,
571 };
572
573 if x_twips.is_none()
574 && y_twips.is_none()
575 && left_from_text_twips.is_none()
576 && right_from_text_twips.is_none()
577 && top_from_text_twips.is_none()
578 && bottom_from_text_twips.is_none()
579 && horizontal_anchor.is_none()
580 && vertical_anchor.is_none()
581 && horizontal_align.is_none()
582 && vertical_align.is_none()
583 {
584 return None;
585 }
586
587 Some(TableFloating {
588 x_twips,
589 y_twips,
590 left_from_text_twips,
591 right_from_text_twips,
592 top_from_text_twips,
593 bottom_from_text_twips,
594 horizontal_anchor,
595 vertical_anchor,
596 horizontal_align,
597 vertical_align,
598 })
599}
600
601pub(crate) fn direct_table_property<'a>(xml: &'a str, name: &str) -> Option<&'a str> {
602 crate::xml::direct_child_tag(xml, name)
603}
604
605pub(crate) fn parse_table_preferred_width(tag: &str) -> Option<TablePreferredWidth> {
606 let raw = crate::xml::get_attribute_ref(tag, "w:w").unwrap_or("0");
607 let raw = raw.trim();
608 if let Some(percent) = raw.strip_suffix('%') {
609 let value = percent.trim().parse::<f64>().ok()?;
610 return (value.is_finite() && value >= 0.0).then_some(TablePreferredWidth::Pct { value });
611 }
612 match crate::xml::get_attribute_ref(tag, "w:type").unwrap_or("dxa") {
613 "auto" => Some(TablePreferredWidth::Auto),
614 "nil" => Some(TablePreferredWidth::Nil),
615 "pct" => {
616 let value = raw.parse::<i64>().ok()? as f64 / 50.0;
617 (value >= 0.0).then_some(TablePreferredWidth::Pct { value })
618 }
619 "dxa" => {
620 let value = if let Ok(value) = raw.parse::<i64>() {
621 value as f64
622 } else {
623 let units = [
624 ("mm", 1440.0 / 25.4),
625 ("cm", 1440.0 / 2.54),
626 ("in", 1440.0),
627 ("pt", 20.0),
628 ("pc", 240.0),
629 ("pi", 240.0),
630 ];
631 let (number, factor) = units.iter().find_map(|(suffix, factor)| {
632 raw.strip_suffix(suffix).map(|number| (number, *factor))
633 })?;
634 number.parse::<f64>().ok()? * factor
635 };
636 (value.is_finite() && value >= 0.0).then_some(TablePreferredWidth::Dxa { value })
637 }
638 _ => None,
639 }
640}
641
642pub(crate) fn preferred_width_twips(width: &TablePreferredWidth) -> Option<i64> {
643 match width {
644 TablePreferredWidth::Dxa { value } if value.is_finite() && *value >= 0.0 => {
645 Some(value.round() as i64)
646 }
647 _ => None,
648 }
649}
650
651pub fn parse_table_style_properties_from_xml(
653 table_properties_xml: Option<&str>,
654) -> Option<ParsedTableProperties> {
655 let table_properties_xml = table_properties_xml?;
656
657 let preferred_width =
658 direct_table_property(table_properties_xml, "w:tblW").and_then(parse_table_preferred_width);
659 let width_twips = preferred_width.as_ref().and_then(preferred_width_twips);
660 let alignment = direct_table_property(table_properties_xml, "w:jc")
661 .and_then(|tag| get_attribute(tag, "w:val"))
662 .and_then(|value| match value.as_str() {
663 "left" => Some(TableAlignment::Left),
664 "center" => Some(TableAlignment::Center),
665 "right" => Some(TableAlignment::Right),
666 _ => None,
667 });
668 let bidi_visual = direct_table_property(table_properties_xml, "w:bidiVisual")
669 .and_then(|tag| parse_on_off_attribute(tag, "bidiVisual"));
670
671 let table_indent_tag = super::scan::find_tag_token_ref(table_properties_xml, "w:tblInd");
672 let table_indent_type = table_indent_tag
673 .as_deref()
674 .and_then(|tag| get_attribute(tag, "w:type"))
675 .map(|value| value.to_ascii_lowercase());
676 let table_indent_raw = table_indent_tag
677 .as_deref()
678 .and_then(|tag| parse_integer_attribute(tag, "w:w"));
679 let indent_twips = if table_indent_type.as_deref() == Some("dxa")
680 && table_indent_raw.is_some_and(|value| value != 0)
681 {
682 table_indent_raw
683 } else {
684 None
685 };
686
687 let table_layout_tag = super::scan::find_tag_token_ref(table_properties_xml, "w:tblLayout");
688 let table_layout_raw = table_layout_tag
689 .as_deref()
690 .and_then(|tag| get_attribute(tag, "w:type"))
691 .map(|value| value.to_ascii_lowercase());
692 let layout = match table_layout_raw.as_deref() {
693 Some("fixed") => Some(TableLayout::Fixed),
694 Some("autofit") => Some(TableLayout::Autofit),
695 _ => None,
696 };
697
698 let table_cell_spacing_tag =
699 super::scan::find_tag_token_ref(table_properties_xml, "w:tblCellSpacing");
700 let table_cell_spacing_type = table_cell_spacing_tag
701 .as_deref()
702 .and_then(|tag| get_attribute(tag, "w:type"))
703 .map(|value| value.to_ascii_lowercase());
704 let table_cell_spacing_raw = table_cell_spacing_tag
705 .as_deref()
706 .and_then(|tag| parse_integer_attribute(tag, "w:w"));
707 let cell_spacing_twips = if table_cell_spacing_type.as_deref() == Some("dxa")
708 && table_cell_spacing_raw.is_some_and(|value| value >= 0)
709 {
710 table_cell_spacing_raw
711 } else {
712 None
713 };
714
715 let table_cell_margin_xml =
716 crate::xml::extract_balanced_tag_slices(table_properties_xml, "w:tblCellMar")
717 .into_iter()
718 .next()
719 .or_else(|| super::scan::find_tag_token_ref(table_properties_xml, "w:tblCellMar"));
720 let cell_margin_twips = table_cell_margin_xml
721 .as_deref()
722 .and_then(parse_table_box_spacing);
723 let floating = parse_floating_table_style(Some(table_properties_xml));
724
725 let properties = ParsedTableProperties {
726 width_twips,
727 preferred_width,
728 alignment,
729 bidi_visual,
730 indent_twips,
731 layout,
732 cell_spacing_twips,
733 cell_margin_twips,
734 floating,
735 };
736
737 if has_table_properties(&properties) {
738 Some(properties)
739 } else {
740 None
741 }
742}
743
744pub(crate) fn merge_table_style_properties(
745 inherited: Option<&ParsedTableProperties>,
746 direct: Option<&ParsedTableProperties>,
747) -> Option<ParsedTableProperties> {
748 if inherited.is_none() && direct.is_none() {
749 return None;
750 }
751
752 let preferred_width = direct
753 .and_then(|value| value.preferred_width.clone())
754 .or_else(|| inherited.and_then(|value| value.preferred_width.clone()));
755 let merged = ParsedTableProperties {
756 width_twips: preferred_width.as_ref().and_then(preferred_width_twips),
757 preferred_width,
758 alignment: direct
759 .and_then(|value| value.alignment)
760 .or_else(|| inherited.and_then(|value| value.alignment)),
761 bidi_visual: direct
762 .and_then(|value| value.bidi_visual)
763 .or_else(|| inherited.and_then(|value| value.bidi_visual)),
764 indent_twips: direct
765 .and_then(|value| value.indent_twips)
766 .or_else(|| inherited.and_then(|value| value.indent_twips)),
767 layout: direct
768 .and_then(|value| value.layout)
769 .or_else(|| inherited.and_then(|value| value.layout)),
770 cell_spacing_twips: direct
771 .and_then(|value| value.cell_spacing_twips)
772 .or_else(|| inherited.and_then(|value| value.cell_spacing_twips)),
773 floating: direct
774 .and_then(|value| value.floating.clone())
775 .or_else(|| inherited.and_then(|value| value.floating.clone())),
776 cell_margin_twips: direct
777 .and_then(|value| value.cell_margin_twips.clone())
778 .or_else(|| inherited.and_then(|value| value.cell_margin_twips.clone())),
779 };
780
781 if has_table_properties(&merged) {
782 Some(merged)
783 } else {
784 None
785 }
786}
787
788fn merge_table_look(
789 direct: Option<&ParsedTableLook>,
790 inherited: Option<&ParsedTableLook>,
791) -> ParsedTableLook {
792 let mut merged = DEFAULT_TABLE_LOOK;
793 if let Some(inherited) = inherited {
794 merged = inherited.clone();
795 }
796 if let Some(direct) = direct {
797 merged = direct.clone();
798 }
799 merged
800}
801
802fn normalize_table_conditional_style_type(
803 value: Option<&str>,
804) -> Option<TableConditionalStyleType> {
805 match value?.trim().to_ascii_lowercase().as_str() {
806 "wholtable" | "wholetable" => Some(TableConditionalStyleType::WholeTable),
807 "firstrow" => Some(TableConditionalStyleType::FirstRow),
808 "lastrow" => Some(TableConditionalStyleType::LastRow),
809 "firstcol" => Some(TableConditionalStyleType::FirstCol),
810 "lastcol" => Some(TableConditionalStyleType::LastCol),
811 "band1horz" => Some(TableConditionalStyleType::Band1Horz),
812 "band2horz" => Some(TableConditionalStyleType::Band2Horz),
813 "band1vert" => Some(TableConditionalStyleType::Band1Vert),
814 "band2vert" => Some(TableConditionalStyleType::Band2Vert),
815 "nwcell" => Some(TableConditionalStyleType::NwCell),
816 "necell" => Some(TableConditionalStyleType::NeCell),
817 "swcell" => Some(TableConditionalStyleType::SwCell),
818 "secell" => Some(TableConditionalStyleType::SeCell),
819 _ => None,
820 }
821}
822
823fn merge_table_border_sets(
824 inherited: Option<&TableBorderSet>,
825 direct: Option<&TableBorderSet>,
826) -> Option<TableBorderSet> {
827 if inherited.is_none() && direct.is_none() {
828 return None;
829 }
830
831 let merged = TableBorderSet {
832 top: direct
833 .and_then(|value| value.top.clone())
834 .or_else(|| inherited.and_then(|value| value.top.clone())),
835 right: direct
836 .and_then(|value| value.right.clone())
837 .or_else(|| inherited.and_then(|value| value.right.clone())),
838 bottom: direct
839 .and_then(|value| value.bottom.clone())
840 .or_else(|| inherited.and_then(|value| value.bottom.clone())),
841 left: direct
842 .and_then(|value| value.left.clone())
843 .or_else(|| inherited.and_then(|value| value.left.clone())),
844 inside_h: direct
845 .and_then(|value| value.inside_h.clone())
846 .or_else(|| inherited.and_then(|value| value.inside_h.clone())),
847 inside_v: direct
848 .and_then(|value| value.inside_v.clone())
849 .or_else(|| inherited.and_then(|value| value.inside_v.clone())),
850 tl2br: direct
851 .and_then(|value| value.tl2br.clone())
852 .or_else(|| inherited.and_then(|value| value.tl2br.clone())),
853 tr2bl: direct
854 .and_then(|value| value.tr2bl.clone())
855 .or_else(|| inherited.and_then(|value| value.tr2bl.clone())),
856 };
857
858 if merged.top.is_none()
859 && merged.right.is_none()
860 && merged.bottom.is_none()
861 && merged.left.is_none()
862 && merged.inside_h.is_none()
863 && merged.inside_v.is_none()
864 && merged.tl2br.is_none()
865 && merged.tr2bl.is_none()
866 {
867 return None;
868 }
869
870 Some(merged)
871}
872
873pub fn parse_table_look(table_properties_xml: Option<&str>) -> Option<ParsedTableLook> {
875 let table_properties_xml = table_properties_xml?;
876 let table_look_tag = super::scan::find_tag_token_ref(table_properties_xml, "w:tblLook")?;
877 let look_mask_raw = get_attribute(&table_look_tag, "w:val");
878 let look_mask = look_mask_raw
879 .as_deref()
880 .and_then(|raw| i64::from_str_radix(raw, 16).ok());
881
882 let row_band_size_tag = super::scan::find_tag_token_ref(
883 table_properties_xml,
884 "w:tblStyleRowBandSize",
885 )
886 .or_else(|| {
887 crate::xml::extract_balanced_tag_slices(table_properties_xml, "w:tblStyleRowBandSize")
888 .into_iter()
889 .next()
890 });
891 let col_band_size_tag = super::scan::find_tag_token_ref(
892 table_properties_xml,
893 "w:tblStyleColBandSize",
894 )
895 .or_else(|| {
896 crate::xml::extract_balanced_tag_slices(table_properties_xml, "w:tblStyleColBandSize")
897 .into_iter()
898 .next()
899 });
900 let row_band_size_raw = row_band_size_tag
901 .as_deref()
902 .and_then(|tag| parse_integer_attribute(tag, "w:val"))
903 .unwrap_or(1);
904 let col_band_size_raw = col_band_size_tag
905 .as_deref()
906 .and_then(|tag| parse_integer_attribute(tag, "w:val"))
907 .unwrap_or(1);
908
909 let resolve_on_off_attribute = |attribute: &str| -> Option<bool> {
910 let value = get_attribute(&table_look_tag, attribute)?.to_ascii_lowercase();
911 match value.as_str() {
912 "1" | "true" | "on" => Some(true),
913 "0" | "false" | "off" => Some(false),
914 _ => None,
915 }
916 };
917
918 let mask_value = look_mask.unwrap_or(0);
919
920 Some(ParsedTableLook {
921 first_row: resolve_on_off_attribute("w:firstRow").unwrap_or((mask_value & 0x0020) != 0),
922 last_row: resolve_on_off_attribute("w:lastRow").unwrap_or((mask_value & 0x0040) != 0),
923 first_col: resolve_on_off_attribute("w:firstColumn").unwrap_or((mask_value & 0x0080) != 0),
924 last_col: resolve_on_off_attribute("w:lastColumn").unwrap_or((mask_value & 0x0100) != 0),
925 no_h_band: resolve_on_off_attribute("w:noHBand").unwrap_or((mask_value & 0x0200) != 0),
926 no_v_band: resolve_on_off_attribute("w:noVBand").unwrap_or((mask_value & 0x0400) != 0),
927 row_band_size: row_band_size_raw.max(1),
928 col_band_size: col_band_size_raw.max(1),
929 })
930}
931
932pub fn parse_table_conditional_style_from_xml(
934 xml: &str,
935 theme_fonts: &ThemeFontMap,
936) -> Option<ParsedTableStyleCondition> {
937 if xml.is_empty() {
938 return None;
939 }
940
941 let table_properties_xml = direct_table_property(xml, "w:tblPr").unwrap_or_default();
942 let paragraph_properties_xml = crate::xml::extract_balanced_tag_slices(xml, "w:pPr")
943 .into_iter()
944 .next()
945 .unwrap_or_default();
946 let row_properties_xml = crate::xml::extract_balanced_tag_slices(xml, "w:trPr")
947 .into_iter()
948 .next()
949 .unwrap_or_default();
950 let cell_properties_xml = direct_table_property(xml, "w:tcPr").unwrap_or_default();
951 let cell_preferred_width =
952 direct_table_property(&cell_properties_xml, "w:tcW").and_then(parse_table_preferred_width);
953 let run_properties_xml = resolve_style_properties_block(xml, "w:rPr");
954 let table_look = parse_table_look(Some(&table_properties_xml));
955 let paragraph_text_alignment = super::style::parse_paragraph_text_alignment_from_xml(
956 direct_table_property(xml, "w:pPr").unwrap_or_default(),
957 );
958 let paragraph_snap_to_grid = parse_on_off_attribute(
959 direct_table_property(xml, "w:pPr").unwrap_or_default(),
960 "snapToGrid",
961 );
962 let paragraph_align = map_alignment(
963 super::scan::find_attribute_value_in_tag(¶graph_properties_xml, "w:jc", "w:val")
964 .as_deref(),
965 );
966
967 let row_shading_tag = super::scan::find_tag_token_ref(&row_properties_xml, "w:shd");
968 let cell_shading_tag = super::scan::find_tag_token_ref(&cell_properties_xml, "w:shd")
969 .or_else(|| super::scan::find_tag_token_ref(&table_properties_xml, "w:shd"));
970 let row_background_color = row_shading_tag
971 .as_deref()
972 .and_then(|tag| get_attribute(tag, "w:fill"))
973 .as_deref()
974 .and_then(|value| normalize_hex_color(Some(value)));
975 let cell_background_color = cell_shading_tag
976 .as_deref()
977 .and_then(|tag| get_attribute(tag, "w:fill"))
978 .as_deref()
979 .and_then(|value| normalize_hex_color(Some(value)));
980 let run_style = parse_text_style_from_xml(&run_properties_xml, theme_fonts);
981
982 let table_borders_xml =
983 crate::xml::extract_balanced_tag_slices(&table_properties_xml, "w:tblBorders")
984 .into_iter()
985 .next()
986 .or_else(|| super::scan::find_tag_token_ref(&table_properties_xml, "w:tblBorders"));
987 let cell_borders_xml =
988 crate::xml::extract_balanced_tag_slices(&cell_properties_xml, "w:tcBorders")
989 .into_iter()
990 .next()
991 .or_else(|| super::scan::find_tag_token_ref(&cell_properties_xml, "w:tcBorders"));
992 let table_borders = table_borders_xml
993 .as_deref()
994 .and_then(parse_table_border_set);
995 let cell_borders = cell_borders_xml.as_deref().and_then(parse_table_border_set);
996 let table_properties = parse_table_style_properties_from_xml(Some(&table_properties_xml));
997
998 if row_background_color.is_none()
999 && cell_background_color.is_none()
1000 && cell_preferred_width.is_none()
1001 && paragraph_align.is_none()
1002 && paragraph_snap_to_grid.is_none()
1003 && paragraph_text_alignment.is_none()
1004 && run_style.is_none()
1005 && table_borders.is_none()
1006 && cell_borders.is_none()
1007 && table_properties.is_none()
1008 && table_look.is_none()
1009 {
1010 return None;
1011 }
1012
1013 Some(ParsedTableStyleCondition {
1014 row_background_color,
1015 cell_background_color,
1016 cell_preferred_width,
1017 paragraph_align,
1018 paragraph_snap_to_grid,
1019 paragraph_text_alignment,
1020 run_style,
1021 table_borders,
1022 cell_borders,
1023 table_properties,
1024 table_look,
1025 })
1026}
1027
1028fn merge_table_conditional_style(
1029 inherited: Option<&ParsedTableStyleCondition>,
1030 direct: Option<&ParsedTableStyleCondition>,
1031) -> Option<ParsedTableStyleCondition> {
1032 if inherited.is_none() && direct.is_none() {
1033 return None;
1034 }
1035
1036 let merged = ParsedTableStyleCondition {
1037 row_background_color: direct
1038 .and_then(|value| value.row_background_color.clone())
1039 .or_else(|| inherited.and_then(|value| value.row_background_color.clone())),
1040 cell_background_color: direct
1041 .and_then(|value| value.cell_background_color.clone())
1042 .or_else(|| inherited.and_then(|value| value.cell_background_color.clone())),
1043 cell_preferred_width: direct
1044 .and_then(|value| value.cell_preferred_width.clone())
1045 .or_else(|| inherited.and_then(|value| value.cell_preferred_width.clone())),
1046 paragraph_align: direct
1047 .and_then(|value| value.paragraph_align)
1048 .or_else(|| inherited.and_then(|value| value.paragraph_align)),
1049 paragraph_text_alignment: direct
1050 .and_then(|value| value.paragraph_text_alignment)
1051 .or_else(|| inherited.and_then(|value| value.paragraph_text_alignment)),
1052 paragraph_snap_to_grid: direct
1053 .and_then(|value| value.paragraph_snap_to_grid)
1054 .or_else(|| inherited.and_then(|value| value.paragraph_snap_to_grid)),
1055 run_style: merge_text_styles(
1056 inherited.and_then(|value| value.run_style.clone()),
1057 direct.and_then(|value| value.run_style.clone()),
1058 ),
1059 table_borders: merge_table_border_sets(
1060 inherited.and_then(|value| value.table_borders.as_ref()),
1061 direct.and_then(|value| value.table_borders.as_ref()),
1062 ),
1063 cell_borders: merge_table_border_sets(
1064 inherited.and_then(|value| value.cell_borders.as_ref()),
1065 direct.and_then(|value| value.cell_borders.as_ref()),
1066 ),
1067 table_properties: merge_table_style_properties(
1068 inherited.and_then(|value| value.table_properties.as_ref()),
1069 direct.and_then(|value| value.table_properties.as_ref()),
1070 ),
1071 table_look: Some(merge_table_look(
1072 direct.and_then(|value| value.table_look.as_ref()),
1073 inherited.and_then(|value| value.table_look.as_ref()),
1074 )),
1075 };
1076
1077 if merged.row_background_color.is_none()
1078 && merged.cell_background_color.is_none()
1079 && merged.cell_preferred_width.is_none()
1080 && merged.paragraph_align.is_none()
1081 && merged.paragraph_snap_to_grid.is_none()
1082 && merged.paragraph_text_alignment.is_none()
1083 && merged.run_style.is_none()
1084 && merged.table_borders.is_none()
1085 && merged.cell_borders.is_none()
1086 && merged.table_properties.is_none()
1087 && merged.table_look.is_none()
1088 {
1089 return None;
1090 }
1091
1092 Some(merged)
1093}
1094
1095pub fn merge_text_styles(
1097 inherited: Option<TextStyle>,
1098 direct: Option<TextStyle>,
1099) -> Option<TextStyle> {
1100 let mut merged = TextStyle {
1101 bold: None,
1102 italic: None,
1103 underline: None,
1104 strike: None,
1105 all_caps: None,
1106 small_caps: None,
1107 color: None,
1108 highlight: None,
1109 background_color: None,
1110 font_size_pt: None,
1111 font_size_cs_pt: None,
1112 font_family: None,
1113 source_font_family: None,
1114 font_family_ascii: None,
1115 font_family_h_ansi: None,
1116 font_family_east_asia: None,
1117 font_family_cs: None,
1118 font_theme_ascii: None,
1119 font_theme_h_ansi: None,
1120 font_theme_east_asia: None,
1121 font_theme_cs: None,
1122 resolved_font_family_ascii: None,
1123 resolved_font_family_h_ansi: None,
1124 resolved_font_family_east_asia: None,
1125 resolved_font_family_cs: None,
1126 font_hint: None,
1127 language: None,
1128 language_east_asia: None,
1129 language_bidi: None,
1130 right_to_left: None,
1131 complex_script: None,
1132 character_spacing_twips: None,
1133 character_scale_percent: None,
1134 source_character_scale: None,
1135 kerning_min_font_size_pt: None,
1136 ligatures: None,
1137 contextual_alternates: None,
1138 vertical_align: None,
1139 run_border: None,
1140 };
1141
1142 for style in [inherited, direct].into_iter().flatten() {
1143 if style.bold.is_some() {
1144 merged.bold = style.bold;
1145 }
1146 if style.italic.is_some() {
1147 merged.italic = style.italic;
1148 }
1149 if style.underline.is_some() {
1150 merged.underline = style.underline;
1151 }
1152 if style.strike.is_some() {
1153 merged.strike = style.strike;
1154 }
1155 if style.all_caps.is_some() {
1156 merged.all_caps = style.all_caps;
1157 }
1158 if style.small_caps.is_some() {
1159 merged.small_caps = style.small_caps;
1160 }
1161 if style.color.is_some() {
1162 merged.color = style.color;
1163 }
1164 if style.highlight.is_some() {
1165 merged.highlight = style.highlight;
1166 }
1167 if style.background_color.is_some() {
1168 merged.background_color = style.background_color;
1169 }
1170 if style.font_size_pt.is_some() {
1171 merged.font_size_pt = style.font_size_pt;
1172 }
1173 if style.font_size_cs_pt.is_some() {
1174 merged.font_size_cs_pt = style.font_size_cs_pt;
1175 }
1176 if style.font_family.is_some() {
1177 merged.font_family = style.font_family;
1178 }
1179 if style.source_font_family.is_some() {
1180 merged.source_font_family = style.source_font_family;
1181 }
1182 if style.font_family_ascii.is_some() {
1183 merged.font_family_ascii = style.font_family_ascii;
1184 }
1185 if style.font_family_h_ansi.is_some() {
1186 merged.font_family_h_ansi = style.font_family_h_ansi;
1187 }
1188 if style.font_family_east_asia.is_some() {
1189 merged.font_family_east_asia = style.font_family_east_asia;
1190 }
1191 if style.font_family_cs.is_some() {
1192 merged.font_family_cs = style.font_family_cs;
1193 }
1194 if style.font_theme_ascii.is_some() {
1195 merged.font_theme_ascii = style.font_theme_ascii;
1196 }
1197 if style.font_theme_h_ansi.is_some() {
1198 merged.font_theme_h_ansi = style.font_theme_h_ansi;
1199 }
1200 if style.font_theme_east_asia.is_some() {
1201 merged.font_theme_east_asia = style.font_theme_east_asia;
1202 }
1203 if style.font_theme_cs.is_some() {
1204 merged.font_theme_cs = style.font_theme_cs;
1205 }
1206 if style.resolved_font_family_ascii.is_some() {
1207 merged.resolved_font_family_ascii = style.resolved_font_family_ascii;
1208 }
1209 if style.resolved_font_family_h_ansi.is_some() {
1210 merged.resolved_font_family_h_ansi = style.resolved_font_family_h_ansi;
1211 }
1212 if style.resolved_font_family_east_asia.is_some() {
1213 merged.resolved_font_family_east_asia = style.resolved_font_family_east_asia;
1214 }
1215 if style.resolved_font_family_cs.is_some() {
1216 merged.resolved_font_family_cs = style.resolved_font_family_cs;
1217 }
1218 if style.font_hint.is_some() {
1219 merged.font_hint = style.font_hint;
1220 }
1221 if style.language.is_some() {
1222 merged.language = style.language;
1223 }
1224 if style.language_east_asia.is_some() {
1225 merged.language_east_asia = style.language_east_asia;
1226 }
1227 if style.language_bidi.is_some() {
1228 merged.language_bidi = style.language_bidi;
1229 }
1230 if style.right_to_left.is_some() {
1231 merged.right_to_left = style.right_to_left;
1232 }
1233 if style.complex_script.is_some() {
1234 merged.complex_script = style.complex_script;
1235 }
1236 if style.character_scale_percent.is_some() || style.source_character_scale.is_some() {
1237 merged.character_scale_percent = style.character_scale_percent;
1238 merged.source_character_scale = style.source_character_scale;
1239 }
1240 if style.character_spacing_twips.is_some() {
1241 merged.character_spacing_twips = style.character_spacing_twips;
1242 }
1243 if style.kerning_min_font_size_pt.is_some() {
1244 merged.kerning_min_font_size_pt = style.kerning_min_font_size_pt;
1245 }
1246 if style.ligatures.is_some() {
1247 merged.ligatures = style.ligatures;
1248 }
1249 if style.contextual_alternates.is_some() {
1250 merged.contextual_alternates = style.contextual_alternates;
1251 }
1252 if style.vertical_align.is_some() {
1253 merged.vertical_align = style.vertical_align;
1254 }
1255 if style.run_border.is_some() {
1256 merged.run_border = style.run_border;
1257 }
1258 }
1259
1260 let has_any = merged.bold.is_some()
1261 || merged.italic.is_some()
1262 || merged.underline.is_some()
1263 || merged.strike.is_some()
1264 || merged.all_caps.is_some()
1265 || merged.small_caps.is_some()
1266 || merged.color.is_some()
1267 || merged.highlight.is_some()
1268 || merged.background_color.is_some()
1269 || merged.font_size_pt.is_some()
1270 || merged.font_size_cs_pt.is_some()
1271 || merged.font_family.is_some()
1272 || merged.source_font_family.is_some()
1273 || merged.font_family_ascii.is_some()
1274 || merged.font_family_h_ansi.is_some()
1275 || merged.font_family_east_asia.is_some()
1276 || merged.font_family_cs.is_some()
1277 || merged.font_theme_ascii.is_some()
1278 || merged.font_theme_h_ansi.is_some()
1279 || merged.font_theme_east_asia.is_some()
1280 || merged.font_theme_cs.is_some()
1281 || merged.resolved_font_family_ascii.is_some()
1282 || merged.resolved_font_family_h_ansi.is_some()
1283 || merged.resolved_font_family_east_asia.is_some()
1284 || merged.resolved_font_family_cs.is_some()
1285 || merged.font_hint.is_some()
1286 || merged.language.is_some()
1287 || merged.language_east_asia.is_some()
1288 || merged.language_bidi.is_some()
1289 || merged.right_to_left.is_some()
1290 || merged.complex_script.is_some()
1291 || merged.character_spacing_twips.is_some()
1292 || merged.character_scale_percent.is_some()
1293 || merged.source_character_scale.is_some()
1294 || merged.kerning_min_font_size_pt.is_some()
1295 || merged.ligatures.is_some()
1296 || merged.contextual_alternates.is_some()
1297 || merged.vertical_align.is_some()
1298 || merged.run_border.is_some();
1299
1300 if has_any {
1301 Some(merged)
1302 } else {
1303 None
1304 }
1305}
1306
1307fn merge_paragraph_spacing(
1308 inherited: Option<&ParagraphSpacing>,
1309 direct: Option<&ParagraphSpacing>,
1310) -> Option<ParagraphSpacing> {
1311 if inherited.is_none() && direct.is_none() {
1312 return None;
1313 }
1314
1315 let merged = ParagraphSpacing {
1316 before_twips: direct
1317 .and_then(|value| value.before_twips)
1318 .or_else(|| inherited.and_then(|value| value.before_twips)),
1319 after_twips: direct
1320 .and_then(|value| value.after_twips)
1321 .or_else(|| inherited.and_then(|value| value.after_twips)),
1322 before_auto_spacing: direct
1323 .and_then(|value| value.before_auto_spacing)
1324 .or_else(|| inherited.and_then(|value| value.before_auto_spacing)),
1325 after_auto_spacing: direct
1326 .and_then(|value| value.after_auto_spacing)
1327 .or_else(|| inherited.and_then(|value| value.after_auto_spacing)),
1328 line_twips: direct
1329 .and_then(|value| value.line_twips)
1330 .or_else(|| inherited.and_then(|value| value.line_twips)),
1331 line_rule: direct
1332 .and_then(|value| value.line_rule)
1333 .or_else(|| inherited.and_then(|value| value.line_rule)),
1334 };
1335
1336 if merged.before_twips.is_none()
1337 && merged.after_twips.is_none()
1338 && merged.before_auto_spacing.is_none()
1339 && merged.after_auto_spacing.is_none()
1340 && merged.line_twips.is_none()
1341 && merged.line_rule.is_none()
1342 {
1343 return None;
1344 }
1345
1346 Some(merged)
1347}
1348
1349fn merge_paragraph_indent(
1350 inherited: Option<&ParagraphIndent>,
1351 direct: Option<&ParagraphIndent>,
1352) -> Option<ParagraphIndent> {
1353 if inherited.is_none() && direct.is_none() {
1354 return None;
1355 }
1356
1357 let merged = ParagraphIndent {
1358 left_twips: direct
1359 .and_then(|value| value.left_twips)
1360 .or_else(|| inherited.and_then(|value| value.left_twips)),
1361 right_twips: direct
1362 .and_then(|value| value.right_twips)
1363 .or_else(|| inherited.and_then(|value| value.right_twips)),
1364 first_line_twips: direct
1365 .and_then(|value| value.first_line_twips)
1366 .or_else(|| inherited.and_then(|value| value.first_line_twips)),
1367 hanging_twips: direct
1368 .and_then(|value| value.hanging_twips)
1369 .or_else(|| inherited.and_then(|value| value.hanging_twips)),
1370 };
1371
1372 if merged.left_twips.is_none()
1373 && merged.right_twips.is_none()
1374 && merged.first_line_twips.is_none()
1375 && merged.hanging_twips.is_none()
1376 {
1377 return None;
1378 }
1379
1380 Some(merged)
1381}
1382
1383fn merge_paragraph_background_color(
1384 inherited: Option<&str>,
1385 direct: Option<&str>,
1386) -> Option<String> {
1387 direct
1388 .map(str::to_string)
1389 .or_else(|| inherited.map(str::to_string))
1390}
1391
1392fn merge_paragraph_border_style(
1393 inherited: Option<&ParagraphBorderStyle>,
1394 direct: Option<&ParagraphBorderStyle>,
1395) -> Option<ParagraphBorderStyle> {
1396 if inherited.is_none() && direct.is_none() {
1397 return None;
1398 }
1399
1400 let border_type = direct
1401 .map(|value| value.border_type.clone())
1402 .or_else(|| inherited.map(|value| value.border_type.clone()))?;
1403 if border_type.is_empty() {
1404 return None;
1405 }
1406
1407 Some(ParagraphBorderStyle {
1408 border_type,
1409 color: direct
1410 .and_then(|value| value.color.clone())
1411 .or_else(|| inherited.and_then(|value| value.color.clone())),
1412 size_eighth_pt: direct
1413 .and_then(|value| value.size_eighth_pt)
1414 .or_else(|| inherited.and_then(|value| value.size_eighth_pt)),
1415 space_pt: direct
1416 .and_then(|value| value.space_pt)
1417 .or_else(|| inherited.and_then(|value| value.space_pt)),
1418 frame: direct
1419 .and_then(|value| value.frame)
1420 .or_else(|| inherited.and_then(|value| value.frame)),
1421 shadow: direct
1422 .and_then(|value| value.shadow)
1423 .or_else(|| inherited.and_then(|value| value.shadow)),
1424 })
1425}
1426
1427fn merge_paragraph_border_sets(
1428 inherited: Option<&ParagraphBorderSet>,
1429 direct: Option<&ParagraphBorderSet>,
1430) -> Option<ParagraphBorderSet> {
1431 if inherited.is_none() && direct.is_none() {
1432 return None;
1433 }
1434
1435 let merged = ParagraphBorderSet {
1436 top: merge_paragraph_border_style(
1437 inherited.and_then(|value| value.top.as_ref()),
1438 direct.and_then(|value| value.top.as_ref()),
1439 ),
1440 right: merge_paragraph_border_style(
1441 inherited.and_then(|value| value.right.as_ref()),
1442 direct.and_then(|value| value.right.as_ref()),
1443 ),
1444 bottom: merge_paragraph_border_style(
1445 inherited.and_then(|value| value.bottom.as_ref()),
1446 direct.and_then(|value| value.bottom.as_ref()),
1447 ),
1448 left: merge_paragraph_border_style(
1449 inherited.and_then(|value| value.left.as_ref()),
1450 direct.and_then(|value| value.left.as_ref()),
1451 ),
1452 between: merge_paragraph_border_style(
1453 inherited.and_then(|value| value.between.as_ref()),
1454 direct.and_then(|value| value.between.as_ref()),
1455 ),
1456 bar: merge_paragraph_border_style(
1457 inherited.and_then(|value| value.bar.as_ref()),
1458 direct.and_then(|value| value.bar.as_ref()),
1459 ),
1460 };
1461
1462 if merged.top.is_none()
1463 && merged.right.is_none()
1464 && merged.bottom.is_none()
1465 && merged.left.is_none()
1466 && merged.between.is_none()
1467 && merged.bar.is_none()
1468 {
1469 return None;
1470 }
1471
1472 Some(merged)
1473}
1474
1475fn merge_paragraph_tab_stops(
1476 inherited: Option<&[ParagraphTabStop]>,
1477 direct: Option<&[ParagraphTabStop]>,
1478) -> Option<Vec<ParagraphTabStop>> {
1479 if inherited.is_none() && direct.is_none() {
1480 return None;
1481 }
1482
1483 let mut by_position = HashMap::new();
1484 for stop in inherited.unwrap_or(&[]) {
1485 if let Some(position) = stop.position_twips {
1486 by_position.insert(position, stop.clone());
1487 }
1488 }
1489 for stop in direct.unwrap_or(&[]) {
1490 if let Some(position) = stop.position_twips {
1491 by_position.insert(position, stop.clone());
1492 }
1493 }
1494
1495 if by_position.is_empty() {
1496 return None;
1497 }
1498
1499 let mut combined: Vec<ParagraphTabStop> = by_position.into_values().collect();
1500 combined.sort_by_key(|stop| stop.position_twips.unwrap_or(0));
1501 Some(combined)
1502}
1503
1504fn merge_paragraph_boolean(inherited: Option<bool>, direct: Option<bool>) -> Option<bool> {
1505 if direct.is_some() {
1506 direct
1507 } else {
1508 inherited
1509 }
1510}
1511
1512fn resolve_theme_font(theme_token: Option<&str>, theme_fonts: &ThemeFontMap) -> Option<String> {
1513 let normalized = theme_token?.to_ascii_lowercase();
1514 if normalized.starts_with("major") {
1515 if normalized.contains("eastasia") {
1516 return theme_fonts
1517 .major_east_asia
1518 .clone()
1519 .or_else(|| theme_fonts.major_latin.clone());
1520 }
1521 if normalized.contains("bidi") || normalized.contains("cs") {
1522 return theme_fonts
1523 .major_complex_script
1524 .clone()
1525 .or_else(|| theme_fonts.major_latin.clone());
1526 }
1527 return theme_fonts.major_latin.clone();
1528 }
1529 if normalized.starts_with("minor") {
1530 if normalized.contains("eastasia") {
1531 return theme_fonts
1532 .minor_east_asia
1533 .clone()
1534 .or_else(|| theme_fonts.minor_latin.clone());
1535 }
1536 if normalized.contains("bidi") || normalized.contains("cs") {
1537 return theme_fonts
1538 .minor_complex_script
1539 .clone()
1540 .or_else(|| theme_fonts.minor_latin.clone());
1541 }
1542 return theme_fonts.minor_latin.clone();
1543 }
1544 None
1545}
1546
1547fn find_drawing_rpr_attribute(xml: &str, attribute: &str) -> Option<String> {
1548 let tag = super::scan::find_tag_token_ref(xml, "a:rPr")?;
1549 get_attribute(&tag, attribute)
1550}
1551
1552fn find_drawing_latin_typeface(xml: &str) -> Option<String> {
1553 if let Some(block) = crate::xml::extract_balanced_tag_slices(xml, "a:rPr")
1554 .into_iter()
1555 .next()
1556 {
1557 if let Some(latin) = crate::xml::extract_balanced_tag_slices(&block, "a:latin")
1558 .into_iter()
1559 .next()
1560 .or_else(|| super::scan::find_tag_token_ref(&block, "a:latin"))
1561 {
1562 if let Some(typeface) = get_attribute(&latin, "typeface") {
1563 return Some(typeface);
1564 }
1565 }
1566 }
1567
1568 if let Some(block) = crate::xml::extract_balanced_tag_slices(xml, "a:defRPr")
1569 .into_iter()
1570 .next()
1571 {
1572 if let Some(latin) = crate::xml::extract_balanced_tag_slices(&block, "a:latin")
1573 .into_iter()
1574 .next()
1575 .or_else(|| super::scan::find_tag_token_ref(&block, "a:latin"))
1576 {
1577 if let Some(typeface) = get_attribute(&latin, "typeface") {
1578 return Some(typeface);
1579 }
1580 }
1581 }
1582
1583 super::scan::find_tag_token_ref(xml, "a:latin")
1584 .and_then(|latin| get_attribute(&latin, "typeface"))
1585}
1586
1587fn find_drawing_color(xml: &str) -> Option<String> {
1588 if let Some(block) = crate::xml::extract_balanced_tag_slices(xml, "a:rPr")
1589 .into_iter()
1590 .next()
1591 {
1592 if let Some(color_xml) = crate::xml::extract_balanced_tag_slices(&block, "a:solidFill")
1593 .into_iter()
1594 .next()
1595 .or_else(|| super::scan::find_tag_token_ref(&block, "a:solidFill"))
1596 {
1597 if let Some(srgb) = super::scan::find_tag_token_ref(&color_xml, "a:srgbClr") {
1598 if let Some(color) = get_attribute(&srgb, "val")
1599 .as_deref()
1600 .and_then(|value| normalize_hex_color(Some(value)))
1601 {
1602 return Some(color);
1603 }
1604 }
1605 }
1606 if let Some(srgb) = super::scan::find_tag_token_ref(&block, "a:srgbClr") {
1607 if let Some(color) = get_attribute(&srgb, "val")
1608 .as_deref()
1609 .and_then(|value| normalize_hex_color(Some(value)))
1610 {
1611 return Some(color);
1612 }
1613 }
1614 }
1615 None
1616}
1617
1618fn contains_east_asia_glyphs(text: &str) -> bool {
1619 text.chars().any(|ch| {
1620 matches!(ch,
1621 '\u{2e80}'..='\u{9fff}'
1622 | '\u{ac00}'..='\u{d7af}'
1623 )
1624 })
1625}
1626
1627fn contains_complex_script_glyphs(text: &str) -> bool {
1628 text.chars().any(|ch| {
1629 matches!(ch,
1630 '\u{0590}'..='\u{08ff}'
1631 | '\u{fb1d}'..='\u{fefc}'
1632 )
1633 })
1634}
1635
1636fn collect_decoded_text_samples(xml: &str) -> String {
1637 let mut samples = String::new();
1638 for tag_name in ["w:t", "a:t"] {
1639 for block in crate::xml::extract_balanced_tag_slices(xml, tag_name) {
1640 if let Some(start) = block.find('>') {
1641 let end = block.rfind("</").unwrap_or(block.len());
1642 if start + 1 < end {
1643 samples.push_str(&decode_xml_entities(&block[start + 1..end]));
1644 }
1645 }
1646 }
1647 }
1648 samples
1649}
1650
1651pub fn parse_text_style_from_xml(xml: &str, theme_fonts: &ThemeFontMap) -> Option<TextStyle> {
1653 super::style::cached_definition_style(xml, theme_fonts)
1654}
1655
1656pub(crate) fn parse_definition_style_uncached(
1657 xml: &str,
1658 theme_fonts: &ThemeFontMap,
1659) -> Option<TextStyle> {
1660 if xml.is_empty() {
1661 return None;
1662 }
1663
1664 let bold = parse_on_off_attribute(xml, "b");
1665 let italic = parse_on_off_attribute(xml, "i");
1666 let underline = parse_underline_attribute(xml);
1667 let strike = parse_on_off_attribute(xml, "strike");
1668 let all_caps = parse_on_off_attribute(xml, "caps");
1669 let small_caps = parse_on_off_attribute(xml, "smallCaps");
1670
1671 let color_match = super::scan::find_attribute_value_in_tag(xml, "w:color", "w:val");
1672 let highlight_match = super::scan::find_attribute_value_in_tag(xml, "w:highlight", "w:val");
1673 let shading_tag = super::scan::find_tag_token_ref(xml, "w:shd");
1674 let character_spacing_match =
1675 super::scan::find_attribute_value_in_tag(xml, "w:spacing", "w:val");
1676 let kerning_match = super::scan::find_attribute_value_in_tag(xml, "w:kern", "w:val");
1677 let size_match = super::scan::find_attribute_value_in_tag(xml, "w:sz", "w:val");
1678 let complex_size_match = super::scan::find_attribute_value_in_tag(xml, "w:szCs", "w:val");
1679 let run_fonts_tag = super::scan::find_tag_token_ref(xml, "w:rFonts").unwrap_or_default();
1680 let ascii_font = get_attribute(&run_fonts_tag, "w:ascii");
1681 let h_ansi_font = get_attribute(&run_fonts_tag, "w:hAnsi");
1682 let east_asia_font = get_attribute(&run_fonts_tag, "w:eastAsia");
1683 let complex_script_font = get_attribute(&run_fonts_tag, "w:cs");
1684 let ascii_theme_font = get_attribute(&run_fonts_tag, "w:asciiTheme");
1685 let h_ansi_theme_font = get_attribute(&run_fonts_tag, "w:hAnsiTheme");
1686 let east_asia_theme_font = get_attribute(&run_fonts_tag, "w:eastAsiaTheme");
1687 let complex_script_theme_font = get_attribute(&run_fonts_tag, "w:csTheme");
1688 let font_hint = get_attribute(&run_fonts_tag, "w:hint");
1689 let language_tag = super::scan::find_tag_token_ref(xml, "w:lang").unwrap_or_default();
1690 let language = get_attribute(&language_tag, "w:val");
1691 let language_east_asia = get_attribute(&language_tag, "w:eastAsia");
1692 let language_bidi = get_attribute(&language_tag, "w:bidi");
1693 let right_to_left = parse_on_off_attribute(xml, "rtl");
1694 let complex_script = parse_on_off_attribute(xml, "cs");
1695 let vertical_align_match =
1696 super::scan::find_attribute_value_in_tag(xml, "w:vertAlign", "w:val");
1697 let drawing_bold_match = find_drawing_rpr_attribute(xml, "b");
1698 let drawing_italic_match = find_drawing_rpr_attribute(xml, "i");
1699 let drawing_underline_match = find_drawing_rpr_attribute(xml, "u");
1700 let drawing_strike_match =
1701 find_drawing_rpr_attribute(xml, "strike").or_else(|| find_drawing_rpr_attribute(xml, "s"));
1702 let drawing_color_match = find_drawing_color(xml);
1703 let drawing_size_match = find_drawing_rpr_attribute(xml, "sz");
1704 let drawing_font_match = find_drawing_latin_typeface(xml);
1705 let run_border_tag = super::scan::find_tag_token_ref(xml, "w:bdr");
1706 let decoded_text_samples = collect_decoded_text_samples(xml);
1707 let contains_east_asia = contains_east_asia_glyphs(&decoded_text_samples);
1708 let contains_complex_script = contains_complex_script_glyphs(&decoded_text_samples);
1709
1710 let mut style = TextStyle {
1711 bold: None,
1712 italic: None,
1713 underline: None,
1714 strike: None,
1715 all_caps: None,
1716 small_caps: None,
1717 color: None,
1718 highlight: None,
1719 background_color: None,
1720 font_size_pt: None,
1721 font_size_cs_pt: None,
1722 font_family: None,
1723 source_font_family: None,
1724 font_family_ascii: None,
1725 font_family_h_ansi: None,
1726 font_family_east_asia: None,
1727 font_family_cs: None,
1728 font_theme_ascii: None,
1729 font_theme_h_ansi: None,
1730 font_theme_east_asia: None,
1731 font_theme_cs: None,
1732 resolved_font_family_ascii: None,
1733 resolved_font_family_h_ansi: None,
1734 resolved_font_family_east_asia: None,
1735 resolved_font_family_cs: None,
1736 font_hint: None,
1737 language: None,
1738 language_east_asia: None,
1739 language_bidi: None,
1740 right_to_left: None,
1741 complex_script: None,
1742 character_spacing_twips: None,
1743 character_scale_percent: None,
1744 source_character_scale: None,
1745 kerning_min_font_size_pt: None,
1746 ligatures: super::style::parse_text_ligatures_from_xml(xml),
1747 contextual_alternates: super::style::parse_contextual_alternates_from_xml(xml),
1748 vertical_align: None,
1749 run_border: None,
1750 };
1751
1752 style.character_scale_percent = super::style::parse_character_scale_percent_from_xml(xml);
1753 style.source_character_scale = super::scan::find_attribute_value_in_tag(xml, "w:w", "w:val");
1754 style.bold = bold;
1755 style.italic = italic;
1756 style.underline = underline;
1757 style.strike = strike;
1758 style.all_caps = all_caps;
1759 style.small_caps = small_caps;
1760
1761 if style.bold.is_none() {
1762 if let Some(value) = drawing_bold_match {
1763 let value = value.to_ascii_lowercase();
1764 style.bold = Some(value != "0" && value != "false");
1765 }
1766 }
1767 if style.italic.is_none() {
1768 if let Some(value) = drawing_italic_match {
1769 let value = value.to_ascii_lowercase();
1770 style.italic = Some(value != "0" && value != "false");
1771 }
1772 }
1773 if style.underline.is_none() {
1774 if let Some(value) = drawing_underline_match {
1775 let value = value.to_ascii_lowercase();
1776 style.underline = Some(value != "none" && value != "false" && value != "0");
1777 }
1778 }
1779 if style.strike.is_none() {
1780 if let Some(value) = drawing_strike_match {
1781 let value = value.to_ascii_lowercase();
1782 style.strike =
1783 Some(value != "nostrike" && value != "none" && value != "false" && value != "0");
1784 }
1785 }
1786
1787 if let Some(color) = normalize_hex_color(color_match.as_deref()) {
1788 style.color = Some(color);
1789 } else if let Some(color) = drawing_color_match {
1790 style.color = Some(color);
1791 }
1792
1793 if let Some(highlight) = highlight_match {
1794 style.highlight = Some(highlight);
1795 }
1796
1797 if let Some(shading_tag) = shading_tag {
1798 if let Some(shading_fill) =
1799 normalize_hex_color(get_attribute(&shading_tag, "w:fill").as_deref())
1800 {
1801 style.background_color = Some(shading_fill);
1802 }
1803 }
1804
1805 if let Some(character_spacing) = character_spacing_match.and_then(|raw| raw.parse::<i64>().ok())
1806 {
1807 style.character_spacing_twips = Some(character_spacing);
1808 }
1809
1810 if let Some(kerning_raw) = kerning_match.and_then(|raw| raw.parse::<f64>().ok()) {
1811 if kerning_raw.is_finite() && kerning_raw >= 0.0 {
1812 style.kerning_min_font_size_pt = Some(kerning_raw / 2.0);
1813 }
1814 }
1815
1816 if let Some(size_raw) = size_match.and_then(|raw| raw.parse::<f64>().ok()) {
1817 style.font_size_pt = Some(size_raw / 2.0);
1818 } else if let Some(size_raw) = drawing_size_match.and_then(|raw| raw.parse::<f64>().ok()) {
1819 style.font_size_pt = Some(size_raw / 100.0);
1820 }
1821 if let Some(size_raw) = complex_size_match.and_then(|raw| raw.parse::<f64>().ok()) {
1822 style.font_size_cs_pt = Some(size_raw / 2.0);
1823 }
1824
1825 style.font_family_ascii = ascii_font.clone();
1826 style.font_family_h_ansi = h_ansi_font.clone();
1827 style.font_family_east_asia = east_asia_font.clone();
1828 style.font_family_cs = complex_script_font.clone();
1829 style.font_theme_ascii = ascii_theme_font.clone();
1830 style.font_theme_h_ansi = h_ansi_theme_font.clone();
1831 style.font_theme_east_asia = east_asia_theme_font.clone();
1832 style.font_theme_cs = complex_script_theme_font.clone();
1833 style.resolved_font_family_ascii = ascii_theme_font
1834 .as_deref()
1835 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
1836 .or_else(|| ascii_font.clone());
1837 style.resolved_font_family_h_ansi = h_ansi_theme_font
1838 .as_deref()
1839 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
1840 .or_else(|| h_ansi_font.clone());
1841 style.resolved_font_family_east_asia = east_asia_theme_font
1842 .as_deref()
1843 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
1844 .or_else(|| east_asia_font.clone());
1845 style.resolved_font_family_cs = complex_script_theme_font
1846 .as_deref()
1847 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
1848 .or_else(|| complex_script_font.clone());
1849 style.font_hint = font_hint;
1850 style.language = language;
1851 style.language_east_asia = language_east_asia;
1852 style.language_bidi = language_bidi;
1853 style.right_to_left = right_to_left;
1854 style.complex_script = complex_script;
1855
1856 let run_font_family = ascii_font.clone().or(h_ansi_font.clone());
1857 let run_theme_font_token = ascii_theme_font.clone().or(h_ansi_theme_font.clone());
1858 let east_asia_fallback_font = if contains_east_asia {
1859 east_asia_font.clone()
1860 } else {
1861 None
1862 };
1863 let east_asia_fallback_theme_token = if contains_east_asia {
1864 east_asia_theme_font.clone()
1865 } else {
1866 None
1867 };
1868 let complex_script_fallback_font = if contains_complex_script {
1869 complex_script_font.clone()
1870 } else {
1871 None
1872 };
1873 let complex_script_fallback_theme_token = if contains_complex_script {
1874 complex_script_theme_font.clone()
1875 } else {
1876 None
1877 };
1878 let symbol_fallback_font = {
1879 let candidate = east_asia_font.clone().or(complex_script_font.clone());
1880 candidate.filter(|font| {
1881 let lower = font.to_ascii_lowercase();
1882 lower.contains("symbol")
1883 || lower.contains("emoji")
1884 || lower.contains("dingbat")
1885 || lower.contains("wingding")
1886 || lower.contains("webding")
1887 })
1888 };
1889
1890 if let Some(font_family) = run_font_family {
1891 style.font_family = Some(font_family);
1892 } else if let Some(theme_token) = run_theme_font_token.as_deref() {
1893 if let Some(font_family) = resolve_theme_font(Some(theme_token), theme_fonts) {
1894 style.font_family = Some(font_family);
1895 }
1896 } else if let Some(font_family) = east_asia_fallback_font {
1897 style.font_family = Some(font_family);
1898 } else if let Some(theme_token) = east_asia_fallback_theme_token.as_deref() {
1899 if let Some(font_family) = resolve_theme_font(Some(theme_token), theme_fonts) {
1900 style.font_family = Some(font_family);
1901 }
1902 } else if let Some(font_family) = complex_script_fallback_font {
1903 style.font_family = Some(font_family);
1904 } else if let Some(theme_token) = complex_script_fallback_theme_token.as_deref() {
1905 if let Some(font_family) = resolve_theme_font(Some(theme_token), theme_fonts) {
1906 style.font_family = Some(font_family);
1907 }
1908 } else if let Some(font_family) = symbol_fallback_font {
1909 style.font_family = Some(font_family);
1910 } else if let Some(font_family) = drawing_font_match {
1911 style.font_family = Some(font_family);
1912 }
1913 style.source_font_family = style.font_family.clone();
1914
1915 if let Some(vertical_align_value) = vertical_align_match.map(|value| value.to_ascii_lowercase())
1916 {
1917 style.vertical_align = match vertical_align_value.as_str() {
1918 "superscript" => Some(VerticalAlign::Superscript),
1919 "subscript" => Some(VerticalAlign::Subscript),
1920 _ => None,
1921 };
1922 }
1923
1924 style.run_border = parse_text_run_border_style(run_border_tag.as_deref());
1925
1926 merge_text_styles(None, Some(style))
1927}
1928
1929pub fn parse_paragraph_align_from_xml(xml: &str) -> Option<ParagraphAlignment> {
1931 map_alignment(super::scan::find_attribute_value_in_tag(xml, "w:jc", "w:val").as_deref())
1932}
1933
1934pub(crate) fn parse_heading_level_from_outline(xml: &str) -> Option<Option<HeadingLevel>> {
1935 let tag = direct_table_property(xml, "w:outlineLvl")?;
1936 let outline_value = get_attribute(tag, "w:val")?;
1937 let level = outline_value.parse::<i64>().ok()? + 1;
1938 Some(match level {
1939 1 => Some(HeadingLevel::One),
1940 2 => Some(HeadingLevel::Two),
1941 3 => Some(HeadingLevel::Three),
1942 4 => Some(HeadingLevel::Four),
1943 5 => Some(HeadingLevel::Five),
1944 6 => Some(HeadingLevel::Six),
1945 7 => Some(HeadingLevel::Seven),
1946 8 => Some(HeadingLevel::Eight),
1947 9 => Some(HeadingLevel::Nine),
1948 _ => None,
1949 })
1950}
1951
1952fn parse_style_type(value: Option<&str>) -> Option<RawStyleType> {
1953 match value?.to_ascii_lowercase().as_str() {
1954 "paragraph" => Some(RawStyleType::Paragraph),
1955 "character" => Some(RawStyleType::Character),
1956 "numbering" => Some(RawStyleType::Numbering),
1957 "table" => Some(RawStyleType::Table),
1958 _ => None,
1959 }
1960}
1961
1962fn resolve_style_properties_block<'a>(style_xml: &'a str, tag_name: &str) -> &'a str {
1963 crate::xml::extract_balanced_tag_slices(style_xml, tag_name)
1964 .into_iter()
1965 .next()
1966 .or_else(|| super::scan::find_tag_token_ref(style_xml, tag_name))
1967 .unwrap_or_default()
1968}
1969
1970fn strip_table_conditional_style_blocks(style_xml: &str) -> String {
1971 if style_xml.is_empty() {
1972 return style_xml.to_string();
1973 }
1974
1975 let mut output = style_xml.to_string();
1976 loop {
1977 let before_len = output.len();
1978 output = remove_balanced_tag_blocks(&output, "w:tblStylePr");
1979 output = remove_self_closing_tags(&output, "w:tblStylePr");
1980 if output.len() == before_len {
1981 break;
1982 }
1983 }
1984 output
1985}
1986
1987fn remove_balanced_tag_blocks(xml: &str, tag_name: &str) -> String {
1988 let mut output = String::new();
1989 let mut last_index = 0;
1990 for range in extract_balanced_tag_ranges(xml, tag_name) {
1991 output.push_str(&xml[last_index..range.start]);
1992 last_index = range.end;
1993 }
1994 output.push_str(&xml[last_index..]);
1995 output
1996}
1997
1998fn remove_self_closing_tags(xml: &str, tag_name: &str) -> String {
1999 let mut output = String::new();
2000 let mut last_index = 0;
2001 let open_prefix = format!("<{tag_name}");
2002 let mut index = 0;
2003 while index < xml.len() {
2004 if xml.as_bytes().get(index) != Some(&b'<')
2005 || !super::scan::starts_with_ignore_ascii_case(&xml[index..], &open_prefix)
2006 {
2007 index += 1;
2008 continue;
2009 }
2010
2011 let after_prefix = index + open_prefix.len();
2012 let mut cursor = after_prefix;
2013 let bytes = xml.as_bytes();
2014 while cursor < bytes.len() {
2015 if bytes[cursor] == b'>' {
2016 let token = &xml[index..=cursor];
2017 if token.ends_with("/>") || token.ends_with("/ >") {
2018 output.push_str(&xml[last_index..index]);
2019 last_index = cursor + 1;
2020 }
2021 index = cursor + 1;
2022 break;
2023 }
2024 cursor += 1;
2025 }
2026 if cursor >= bytes.len() {
2027 break;
2028 }
2029 }
2030 output.push_str(&xml[last_index..]);
2031 output
2032}
2033
2034fn extract_balanced_tag_ranges(xml: &str, tag_name: &str) -> Vec<crate::xml::TagRange> {
2035 crate::xml::extract_balanced_tag_ranges(xml, tag_name)
2036}
2037
2038fn read_typeface(xml: &str, tag_name: &str) -> Option<String> {
2039 let token = super::scan::find_tag_token_ref(xml, &format!("a:{tag_name}"))?;
2040 let value = get_attribute(&token, "typeface")?.trim().to_string();
2041 if value.is_empty() {
2042 None
2043 } else {
2044 Some(value)
2045 }
2046}
2047
2048pub fn parse_theme_fonts(pkg: &OoxmlPackage) -> ThemeFontMap {
2050 let Some(theme_xml) =
2051 crate::package::related_document_part(pkg, "theme").map(|part| part.content.as_str())
2052 else {
2053 return ThemeFontMap::default();
2054 };
2055
2056 let major_font_xml = crate::xml::extract_balanced_tag_slices(theme_xml, "a:majorFont")
2057 .into_iter()
2058 .next()
2059 .unwrap_or_default();
2060 let minor_font_xml = crate::xml::extract_balanced_tag_slices(theme_xml, "a:minorFont")
2061 .into_iter()
2062 .next()
2063 .unwrap_or_default();
2064
2065 ThemeFontMap {
2066 major_latin: read_typeface(&major_font_xml, "latin"),
2067 major_east_asia: read_typeface(&major_font_xml, "ea"),
2068 major_complex_script: read_typeface(&major_font_xml, "cs"),
2069 minor_latin: read_typeface(&minor_font_xml, "latin"),
2070 minor_east_asia: read_typeface(&minor_font_xml, "ea"),
2071 minor_complex_script: read_typeface(&minor_font_xml, "cs"),
2072 }
2073}
2074
2075fn parse_theme_color_from_tag(color_xml: &str) -> Option<String> {
2076 if let Some(explicit) = super::scan::find_attribute_value_in_tag(color_xml, "a:srgbClr", "val")
2077 .as_deref()
2078 .and_then(|value| normalize_hex_color(Some(value)))
2079 {
2080 return Some(explicit);
2081 }
2082
2083 super::scan::find_attribute_value_in_tag(color_xml, "a:sysClr", "lastClr")
2084 .as_deref()
2085 .and_then(|value| normalize_hex_color(Some(value)))
2086}
2087
2088pub fn parse_theme_colors(pkg: &OoxmlPackage) -> ThemeColorMap {
2090 let Some(theme_xml) =
2091 crate::package::related_document_part(pkg, "theme").map(|part| part.content.as_str())
2092 else {
2093 return ThemeColorMap::new();
2094 };
2095
2096 let color_scheme_xml = crate::xml::extract_balanced_tag_slices(theme_xml, "a:clrScheme")
2097 .into_iter()
2098 .next()
2099 .unwrap_or_default();
2100 if color_scheme_xml.is_empty() {
2101 return ThemeColorMap::new();
2102 }
2103
2104 let color_names = [
2105 "dk1",
2106 "lt1",
2107 "dk2",
2108 "lt2",
2109 "accent1",
2110 "accent2",
2111 "accent3",
2112 "accent4",
2113 "accent5",
2114 "accent6",
2115 "hlink",
2116 "folHlink",
2117 "bg1",
2118 "bg2",
2119 "tx1",
2120 "tx2",
2121 "followedHyperlink",
2122 ];
2123
2124 let mut colors = ThemeColorMap::new();
2125 for name in color_names {
2126 let tag_match =
2127 crate::xml::extract_balanced_tag_slices(&color_scheme_xml, &format!("a:{name}"))
2128 .into_iter()
2129 .next()
2130 .or_else(|| {
2131 super::scan::find_tag_token_ref(&color_scheme_xml, &format!("a:{name}"))
2132 });
2133 let Some(tag_match) = tag_match else {
2134 continue;
2135 };
2136 if let Some(color) = parse_theme_color_from_tag(&tag_match) {
2137 colors.insert(name.to_ascii_lowercase(), color);
2138 }
2139 }
2140
2141 colors
2142}
2143
2144const WORD_DECLARED_DEFAULT_FONT_SIZE_PT: f64 = 10.0;
2146
2147fn with_word_application_defaults(doc_defaults_xml: &str, theme_fonts: &ThemeFontMap) -> String {
2153 let declares_run_defaults = super::scan::contains_tag(doc_defaults_xml, "w:rPrDefault");
2154 let declares_paragraph_defaults = super::scan::contains_tag(doc_defaults_xml, "w:pPrDefault");
2155 if declares_run_defaults && declares_paragraph_defaults {
2156 return doc_defaults_xml.to_string();
2157 }
2158 let mut supplied = String::new();
2159 if !declares_run_defaults {
2160 let fonts = if theme_fonts.minor_latin.is_some() {
2161 r#"<w:rFonts w:asciiTheme="minorHAnsi" w:eastAsiaTheme="minorEastAsia" w:hAnsiTheme="minorHAnsi" w:cstheme="minorBidi"/>"#
2162 } else {
2163 r#"<w:rFonts w:ascii="Aptos" w:hAnsi="Aptos"/>"#
2164 };
2165 supplied.push_str(&format!(
2166 r#"<w:rPrDefault><w:rPr>{fonts}<w:sz w:val="24"/><w:szCs w:val="24"/></w:rPr></w:rPrDefault>"#
2167 ));
2168 }
2169 if !declares_paragraph_defaults {
2170 supplied.push_str(
2171 r#"<w:pPrDefault><w:pPr><w:spacing w:after="160" w:line="276" w:lineRule="auto"/></w:pPr></w:pPrDefault>"#,
2172 );
2173 }
2174 match doc_defaults_xml.rfind("</w:docDefaults>") {
2175 Some(end) => format!(
2176 "{}{}{}",
2177 &doc_defaults_xml[..end],
2178 supplied,
2179 &doc_defaults_xml[end..]
2180 ),
2181 None => format!("<w:docDefaults>{supplied}</w:docDefaults>"),
2182 }
2183}
2184
2185pub fn parse_style_sheet(pkg: &OoxmlPackage) -> ParsedStyleSheet {
2187 parse_style_sheet_selected(pkg, None)
2188}
2189
2190pub(crate) fn parse_document_style_sheet(pkg: &OoxmlPackage) -> ParsedStyleSheet {
2191 parse_referenced_style_sheet(pkg, document_style_references(pkg))
2192}
2193
2194fn document_style_references(pkg: &OoxmlPackage) -> HashSet<String> {
2195 let mut referenced = HashSet::new();
2196 for part in pkg.parts.values() {
2197 for name in ["w:tblStyle", "w:rStyle", "w:pStyle"] {
2198 for tag in crate::xml::extract_balanced_tag_slices(&part.content, name) {
2199 if let Some(id) = get_attribute(tag, "w:val") {
2200 referenced.insert(id);
2201 }
2202 }
2203 }
2204 }
2205 referenced
2206}
2207
2208fn parse_referenced_style_sheet(
2209 pkg: &OoxmlPackage,
2210 mut referenced: HashSet<String>,
2211) -> ParsedStyleSheet {
2212 if let Some(part) = crate::package::related_document_part(pkg, "styles") {
2213 let mut parents = HashMap::new();
2214 for style in crate::xml::extract_balanced_tag_slices(&part.content, "w:style") {
2215 let tag = super::scan::find_tag_token_ref(style, "w:style").unwrap_or("");
2216 let Some(id) = get_attribute(tag, "w:styleId") else {
2217 continue;
2218 };
2219 if let Some(parent) =
2220 super::scan::find_attribute_value_in_tag(style, "w:basedOn", "w:val")
2221 {
2222 parents.insert(id.clone(), parent);
2223 }
2224 let kind = parse_style_type(get_attribute(tag, "w:type").as_deref());
2225 if kind == Some(RawStyleType::Paragraph)
2226 || (kind == Some(RawStyleType::Table)
2227 && get_attribute(tag, "w:default")
2228 .is_some_and(|v| matches!(v.as_str(), "1" | "true" | "on")))
2229 {
2230 referenced.insert(id);
2231 }
2232 }
2233 let mut pending: Vec<_> = referenced.iter().cloned().collect();
2234 while let Some(id) = pending.pop() {
2235 if let Some(parent) = parents.get(&id) {
2236 if referenced.insert(parent.clone()) {
2237 pending.push(parent.clone());
2238 }
2239 }
2240 }
2241 }
2242 parse_style_sheet_selected(pkg, Some(&referenced))
2243}
2244
2245#[cfg(not(target_arch = "wasm32"))]
2246pub(crate) type DocumentStyleSheet = std::sync::Arc<ParsedStyleSheet>;
2247#[cfg(target_arch = "wasm32")]
2248pub(crate) type DocumentStyleSheet = ParsedStyleSheet;
2249
2250#[cfg(not(target_arch = "wasm32"))]
2251struct CachedDocumentStyleSheet {
2252 styles: Option<String>,
2253 theme: Option<String>,
2254 references: Vec<String>,
2255 source_bytes: usize,
2256 parsed: std::sync::Arc<ParsedStyleSheet>,
2257}
2258
2259#[cfg(not(target_arch = "wasm32"))]
2260static DOCUMENT_STYLE_CACHE: std::sync::LazyLock<
2261 super::cache::Snapshot<std::collections::VecDeque<std::sync::Arc<CachedDocumentStyleSheet>>>,
2262> = std::sync::LazyLock::new(|| super::cache::Snapshot::new(std::collections::VecDeque::new()));
2263
2264#[cfg(not(target_arch = "wasm32"))]
2265pub(crate) fn cached_document_style_identity(styles: &ParsedStyleSheet) -> Option<DocumentStyleSheet> {
2266 DOCUMENT_STYLE_CACHE.read(|cache| cache.iter()
2267 .find(|entry| std::ptr::eq(entry.parsed.as_ref(), styles))
2268 .map(|entry| std::sync::Arc::clone(&entry.parsed)))
2269}
2270
2271pub(crate) fn acquire_document_style_sheet(pkg: &OoxmlPackage) -> DocumentStyleSheet {
2272 #[cfg(target_arch = "wasm32")]
2273 {
2274 parse_document_style_sheet(pkg)
2275 }
2276 #[cfg(not(target_arch = "wasm32"))]
2277 {
2278 let styles =
2279 crate::package::related_document_part(pkg, "styles").map(|part| part.content.as_str());
2280 let theme =
2281 crate::package::related_document_part(pkg, "theme").map(|part| part.content.as_str());
2282 if styles
2283 .map_or(0, str::len)
2284 .saturating_add(theme.map_or(0, str::len))
2285 > 512 * 1024
2286 {
2287 return std::sync::Arc::new(parse_document_style_sheet(pkg));
2288 }
2289 let references = document_style_references(pkg);
2290 if references.len() > 512 || references.iter().map(String::len).sum::<usize>() > 32 * 1024 {
2291 return std::sync::Arc::new(parse_referenced_style_sheet(pkg, references));
2292 }
2293 let mut references: Vec<_> = references.into_iter().collect();
2294 references.sort_unstable();
2295 let source_bytes = styles.map_or(0, str::len)
2296 + theme.map_or(0, str::len)
2297 + references.iter().map(String::len).sum::<usize>();
2298 let matches = |entry: &CachedDocumentStyleSheet| {
2299 entry.styles.as_deref() == styles
2300 && entry.theme.as_deref() == theme
2301 && entry.references == references
2302 };
2303 if let Some(parsed) = DOCUMENT_STYLE_CACHE.read(|cache| cache.iter()
2304 .find(|entry| matches(entry)).map(|entry| entry.parsed.clone())) {
2305 return parsed;
2306 }
2307 let parsed = std::sync::Arc::new(parse_referenced_style_sheet(
2308 pkg, references.iter().cloned().collect(),
2309 ));
2310 if source_bytes > 512 * 1024 { return parsed; }
2311 let entry = std::sync::Arc::new(CachedDocumentStyleSheet {
2312 styles: styles.map(str::to_owned),
2313 theme: theme.map(str::to_owned),
2314 references,
2315 source_bytes,
2316 parsed: parsed.clone(),
2317 });
2318 DOCUMENT_STYLE_CACHE.publish(move |cache| {
2319 if cache.iter().any(|existing| existing.styles == entry.styles
2320 && existing.theme == entry.theme && existing.references == entry.references) {
2321 return cache.clone();
2322 }
2323 let mut next = cache.clone();
2324 let mut retained = next.iter().map(|entry| entry.source_bytes).sum::<usize>();
2325 while next.len() >= 4 || retained + entry.source_bytes > 512 * 1024 {
2326 let Some(old) = next.pop_front() else { break };
2327 retained -= old.source_bytes;
2328 }
2329 next.push_back(entry);
2330 next
2331 });
2332 parsed
2333 }
2334}
2335
2336pub(crate) fn release_document_style_metadata(
2337 styles: DocumentStyleSheet,
2338) -> (Vec<crate::model::ParagraphStyleDefinition>, Option<String>) {
2339 #[cfg(not(target_arch = "wasm32"))]
2340 let styles = match std::sync::Arc::try_unwrap(styles) {
2341 Ok(styles) => styles,
2342 Err(styles) => {
2343 return (
2344 styles.paragraph_styles.clone(),
2345 styles.default_paragraph_style_id.clone(),
2346 )
2347 }
2348 };
2349 (styles.paragraph_styles, styles.default_paragraph_style_id)
2350}
2351
2352fn parse_style_sheet_selected(
2353 pkg: &OoxmlPackage,
2354 selected_styles: Option<&HashSet<String>>,
2355) -> ParsedStyleSheet {
2356 let Some(styles_xml) =
2357 crate::package::related_document_part(pkg, "styles").map(|part| part.content.as_str())
2358 else {
2359 return empty_style_sheet();
2360 };
2361
2362 let theme_fonts = parse_theme_fonts(pkg);
2363 let theme_colors = parse_theme_colors(pkg);
2364 let declared_doc_defaults_xml =
2365 crate::xml::extract_balanced_tag_slices(styles_xml, "w:docDefaults")
2366 .into_iter()
2367 .next()
2368 .unwrap_or_default();
2369 let declares_run_defaults =
2370 super::scan::contains_tag(&declared_doc_defaults_xml, "w:rPrDefault");
2371 let doc_defaults_xml = with_word_application_defaults(&declared_doc_defaults_xml, &theme_fonts);
2372
2373 let default_paragraph_style = {
2374 let paragraph_defaults = resolve_style_properties_block(&doc_defaults_xml, "w:pPr");
2375 let default_paragraph_has_num_pr =
2376 super::scan::contains_tag(¶graph_defaults, "w:numPr");
2377 let align = parse_paragraph_align_from_xml(¶graph_defaults);
2378 let snap_to_grid = parse_on_off_attribute(¶graph_defaults, "snapToGrid");
2379 let text_alignment = super::style::parse_paragraph_text_alignment_from_xml(
2380 direct_table_property(&doc_defaults_xml, "w:pPrDefault")
2381 .and_then(|xml| direct_table_property(xml, "w:pPr"))
2382 .unwrap_or_default(),
2383 );
2384 let spacing = parse_paragraph_spacing_from_xml(¶graph_defaults);
2385 let indent = parse_paragraph_indent_from_xml(¶graph_defaults);
2386 let background_color = parse_paragraph_shading_from_xml(¶graph_defaults);
2387 let borders = parse_paragraph_border_set_from_xml(¶graph_defaults);
2388 let tab_stops = parse_paragraph_tab_stops_from_xml(¶graph_defaults);
2389 let parsed_default_numbering = parse_paragraph_numbering_from_xml(¶graph_defaults);
2390 let numbering = if default_paragraph_has_num_pr && parsed_default_numbering.is_none() {
2391 Some(ParagraphNumbering { num_id: 0, ilvl: 0 })
2392 } else {
2393 parsed_default_numbering
2394 };
2395 let contextual_spacing = parse_on_off_attribute(¶graph_defaults, "contextualSpacing");
2396 let keep_next = parse_on_off_attribute(¶graph_defaults, "keepNext");
2397 let keep_lines = parse_on_off_attribute(¶graph_defaults, "keepLines");
2398 let widow_control = parse_on_off_attribute(¶graph_defaults, "widowControl");
2399 let page_break_before = parse_on_off_attribute(¶graph_defaults, "pageBreakBefore");
2400
2401 if align.is_none()
2402 && snap_to_grid.is_none()
2403 && text_alignment.is_none()
2404 && spacing.is_none()
2405 && indent.is_none()
2406 && background_color.is_none()
2407 && borders.is_none()
2408 && tab_stops.is_empty()
2409 && numbering.is_none()
2410 && contextual_spacing.is_none()
2411 && keep_next.is_none()
2412 && keep_lines.is_none()
2413 && widow_control.is_none()
2414 && page_break_before.is_none()
2415 {
2416 None
2417 } else {
2418 Some(ParagraphStyle {
2419 source_before_auto_spacing: None,
2420 source_after_auto_spacing: None,
2421 align,
2422 snap_to_grid,
2423 source_snap_to_grid: None,
2424 source_has_snap_to_grid: None,
2425 source_inherited_snap_to_grid: None,
2426 source_table_snap_to_grid: None,
2427 text_alignment,
2428 source_text_alignment: None,
2429 source_has_text_alignment: None,
2430 source_inherited_text_alignment: None,
2431 source_table_text_alignment: None,
2432 heading_level: None,
2433 style_id: None,
2434 style_name: None,
2435 numbering,
2436 spacing,
2437 indent,
2438 background_color,
2439 borders,
2440 tab_stops: if tab_stops.is_empty() {
2441 None
2442 } else {
2443 Some(tab_stops)
2444 },
2445 contextual_spacing,
2446 keep_next,
2447 keep_lines,
2448 widow_control,
2449 page_break_before,
2450 drop_cap: None,
2451 })
2452 }
2453 };
2454
2455 let mut default_run_style = parse_text_style_from_xml(
2456 &resolve_style_properties_block(&doc_defaults_xml, "w:rPr"),
2457 &theme_fonts,
2458 );
2459 if declares_run_defaults {
2460 match default_run_style.as_mut() {
2462 Some(style) => {
2463 if style.font_size_pt.is_none() {
2464 style.font_size_pt = Some(WORD_DECLARED_DEFAULT_FONT_SIZE_PT);
2465 }
2466 if style.font_size_cs_pt.is_none() {
2467 style.font_size_cs_pt = Some(WORD_DECLARED_DEFAULT_FONT_SIZE_PT);
2468 }
2469 }
2470 None => {
2471 default_run_style = parse_text_style_from_xml(
2472 r#"<w:rPr><w:sz w:val="20"/><w:szCs w:val="20"/></w:rPr>"#,
2473 &theme_fonts,
2474 );
2475 }
2476 }
2477 }
2478
2479 let mut raw_styles_by_id = HashMap::new();
2480 let mut raw_table_styles_by_id = HashMap::new();
2481 let mut default_table_style_id = None;
2482
2483 for style_xml in crate::xml::extract_balanced_tag_slices(styles_xml, "w:style") {
2484 let style_tag = super::scan::find_tag_token_ref(&style_xml, "w:style").unwrap_or_default();
2485 let style_id = get_attribute(&style_tag, "w:styleId");
2486 let style_type = parse_style_type(get_attribute(&style_tag, "w:type").as_deref());
2487 let (Some(style_id), Some(style_type)) = (style_id, style_type) else {
2488 continue;
2489 };
2490 if selected_styles.is_some_and(|ids| !ids.contains(&style_id)) {
2491 continue;
2492 }
2493
2494 let name_tag = super::scan::find_tag_token_ref(&style_xml, "w:name").unwrap_or_default();
2495 let based_on_tag =
2496 super::scan::find_tag_token_ref(&style_xml, "w:basedOn").unwrap_or_default();
2497 let next_tag = super::scan::find_tag_token_ref(&style_xml, "w:next").unwrap_or_default();
2498 let ui_priority_tag =
2499 super::scan::find_tag_token_ref(&style_xml, "w:uiPriority").unwrap_or_default();
2500 let paragraph_properties_xml = resolve_style_properties_block(&style_xml, "w:pPr");
2501 let style_has_paragraph_num_pr =
2502 super::scan::contains_tag(¶graph_properties_xml, "w:numPr");
2503 let run_properties_xml = resolve_style_properties_block(&style_xml, "w:rPr");
2504
2505 let heading_level = normalize_heading_level(Some(&style_id))
2506 .or_else(|| normalize_heading_level(get_attribute(&name_tag, "w:val").as_deref()));
2507 let outline_heading = parse_heading_level_from_outline(¶graph_properties_xml);
2508
2509 let parsed_style_numbering = parse_paragraph_numbering_from_xml(¶graph_properties_xml);
2510 let tab_stops = parse_paragraph_tab_stops_from_xml(¶graph_properties_xml);
2511
2512 raw_styles_by_id.insert(
2513 style_id.clone(),
2514 RawStyleDefinition {
2515 id: style_id.clone(),
2516 style_type,
2517 name: get_attribute(&name_tag, "w:val").unwrap_or(style_id.clone()),
2518 based_on_id: get_attribute(&based_on_tag, "w:val"),
2519 next_style_id: get_attribute(&next_tag, "w:val"),
2520 align: parse_paragraph_align_from_xml(¶graph_properties_xml),
2521 snap_to_grid: parse_on_off_attribute(¶graph_properties_xml, "snapToGrid"),
2522 text_alignment: super::style::parse_paragraph_text_alignment_from_xml(
2523 direct_table_property(&style_xml, "w:pPr").unwrap_or_default(),
2524 ),
2525 heading_level,
2526 outline_heading,
2527 numbering: if style_has_paragraph_num_pr && parsed_style_numbering.is_none() {
2528 Some(ParagraphNumbering { num_id: 0, ilvl: 0 })
2529 } else {
2530 parsed_style_numbering
2531 },
2532 spacing: parse_paragraph_spacing_from_xml(¶graph_properties_xml),
2533 indent: parse_paragraph_indent_from_xml(¶graph_properties_xml),
2534 background_color: parse_paragraph_shading_from_xml(¶graph_properties_xml),
2535 borders: parse_paragraph_border_set_from_xml(¶graph_properties_xml),
2536 tab_stops: if tab_stops.is_empty() {
2537 None
2538 } else {
2539 Some(tab_stops)
2540 },
2541 contextual_spacing: parse_on_off_attribute(
2542 ¶graph_properties_xml,
2543 "contextualSpacing",
2544 ),
2545 keep_next: parse_on_off_attribute(¶graph_properties_xml, "keepNext"),
2546 keep_lines: parse_on_off_attribute(¶graph_properties_xml, "keepLines"),
2547 widow_control: parse_on_off_attribute(¶graph_properties_xml, "widowControl"),
2548 page_break_before: parse_on_off_attribute(
2549 ¶graph_properties_xml,
2550 "pageBreakBefore",
2551 ),
2552 run_style: parse_text_style_from_xml(&run_properties_xml, &theme_fonts),
2553 ui_priority: get_attribute(&ui_priority_tag, "w:val")
2554 .and_then(|raw| raw.parse::<f64>().ok())
2555 .filter(|value| value.is_finite())
2556 .map(|value| value.round() as i64),
2557 is_default: get_attribute(&style_tag, "w:default")
2558 .is_some_and(|value| value == "1")
2559 .then_some(true),
2560 is_primary: super::scan::contains_tag(&style_xml, "w:qFormat").then_some(true),
2561 },
2562 );
2563
2564 if style_type == RawStyleType::Table
2565 && selected_styles.is_none_or(|ids| ids.contains(&style_id))
2566 {
2567 if get_attribute(&style_tag, "w:default")
2568 .is_some_and(|value| matches!(value.as_str(), "1" | "true" | "on"))
2569 {
2570 default_table_style_id = Some(style_id.clone());
2571 }
2572 let mut conditions = HashMap::new();
2573 let whole_table_source_xml = strip_table_conditional_style_blocks(&style_xml);
2574 if let Some(whole_table_condition) =
2575 parse_table_conditional_style_from_xml(&whole_table_source_xml, &theme_fonts)
2576 {
2577 conditions.insert(TableConditionalStyleType::WholeTable, whole_table_condition);
2578 }
2579
2580 for conditional_style_xml in
2581 crate::xml::extract_balanced_tag_slices(&style_xml, "w:tblStylePr")
2582 {
2583 let conditional_style_tag =
2584 super::scan::find_tag_token_ref(&conditional_style_xml, "w:tblStylePr")
2585 .unwrap_or_default();
2586 let Some(conditional_type) = normalize_table_conditional_style_type(
2587 get_attribute(&conditional_style_tag, "w:type").as_deref(),
2588 ) else {
2589 continue;
2590 };
2591 if let Some(condition) =
2592 parse_table_conditional_style_from_xml(&conditional_style_xml, &theme_fonts)
2593 {
2594 conditions.insert(conditional_type, condition);
2595 }
2596 }
2597
2598 raw_table_styles_by_id.insert(
2599 style_id.clone(),
2600 ParsedTableStyleDefinition {
2601 id: style_id.clone(),
2602 based_on_id: get_attribute(&based_on_tag, "w:val"),
2603 name: get_attribute(&name_tag, "w:val").unwrap_or(style_id),
2604 conditions,
2605 floating: None,
2606 properties: None,
2607 has_row_cell_spacing: crate::xml::extract_balanced_tag_slices(
2608 &style_xml, "w:trPr",
2609 )
2610 .into_iter()
2611 .any(|properties| {
2612 direct_table_property(properties, "w:tblCellSpacing").is_some()
2613 }),
2614 },
2615 );
2616 }
2617 }
2618
2619 let mut run_style_cache = HashMap::new();
2620 fn resolve_run_style(
2621 style_id: &str,
2622 raw_styles_by_id: &HashMap<String, RawStyleDefinition>,
2623 run_style_cache: &mut HashMap<String, Option<TextStyle>>,
2624 stack: &mut HashSet<String>,
2625 ) -> Option<TextStyle> {
2626 if let Some(cached) = run_style_cache.get(style_id) {
2627 return cached.to_owned();
2628 }
2629 if stack.contains(style_id) {
2630 return None;
2631 }
2632 stack.insert(style_id.to_string());
2633
2634 let style = raw_styles_by_id.get(style_id)?;
2635 let inherited = style.based_on_id.as_deref().and_then(|based_on_id| {
2636 resolve_run_style(based_on_id, raw_styles_by_id, run_style_cache, stack)
2637 });
2638 let resolved = merge_text_styles(inherited, style.run_style.clone());
2639 run_style_cache.insert(style_id.to_string(), resolved.clone());
2640 stack.remove(style_id);
2641 resolved
2642 }
2643
2644 let mut paragraph_style_cache = HashMap::new();
2645 fn resolve_paragraph_style(
2646 style_id: &str,
2647 raw_styles_by_id: &HashMap<String, RawStyleDefinition>,
2648 paragraph_style_cache: &mut HashMap<String, Option<ParagraphStyleDefinition>>,
2649 stack: &mut HashSet<String>,
2650 ) -> Option<ParagraphStyleDefinition> {
2651 if let Some(cached) = paragraph_style_cache.get(style_id) {
2652 return cached.to_owned();
2653 }
2654 if stack.contains(style_id) {
2655 return None;
2656 }
2657 stack.insert(style_id.to_string());
2658
2659 let style = raw_styles_by_id.get(style_id)?;
2660 if style.style_type != RawStyleType::Paragraph {
2661 paragraph_style_cache.insert(style_id.to_string(), None);
2662 stack.remove(style_id);
2663 return None;
2664 }
2665
2666 let inherited = style.based_on_id.as_deref().and_then(|based_on_id| {
2667 raw_styles_by_id
2668 .get(based_on_id)
2669 .filter(|parent| parent.style_type == RawStyleType::Paragraph)
2670 .and_then(|_| {
2671 resolve_paragraph_style(
2672 based_on_id,
2673 raw_styles_by_id,
2674 paragraph_style_cache,
2675 stack,
2676 )
2677 })
2678 });
2679
2680 let mut outline_style = Some(style);
2681 let mut outline_seen = HashSet::new();
2682 let mut outline_heading = None;
2683 while let Some(current) = outline_style {
2684 if !outline_seen.insert(current.id.as_str()) {
2685 break;
2686 }
2687 if current.outline_heading.is_some() {
2688 outline_heading = current.outline_heading;
2689 break;
2690 }
2691 outline_style = current
2692 .based_on_id
2693 .as_deref()
2694 .and_then(|id| raw_styles_by_id.get(id))
2695 .filter(|parent| parent.style_type == RawStyleType::Paragraph);
2696 }
2697
2698 let resolved = ParagraphStyleDefinition {
2699 id: style.id.clone(),
2700 name: style.name.clone(),
2701 based_on_id: style.based_on_id.clone(),
2702 next_style_id: style.next_style_id.clone(),
2703 align: style
2704 .align
2705 .or(inherited.as_ref().and_then(|value| value.align)),
2706 snap_to_grid: style
2707 .snap_to_grid
2708 .or(inherited.as_ref().and_then(|value| value.snap_to_grid)),
2709 source_has_snap_to_grid: Some(
2710 style
2711 .snap_to_grid
2712 .or(inherited.as_ref().and_then(|value| value.snap_to_grid))
2713 .is_some(),
2714 ),
2715 text_alignment: style
2716 .text_alignment
2717 .or(inherited.as_ref().and_then(|value| value.text_alignment)),
2718 source_has_text_alignment: Some(
2719 style
2720 .text_alignment
2721 .or(inherited.as_ref().and_then(|value| value.text_alignment))
2722 .is_some(),
2723 ),
2724 heading_level: outline_heading.unwrap_or_else(|| {
2725 style
2726 .heading_level
2727 .or(inherited.as_ref().and_then(|value| value.heading_level))
2728 }),
2729 numbering: style
2730 .numbering
2731 .clone()
2732 .or_else(|| inherited.as_ref().and_then(|value| value.numbering.clone())),
2733 spacing: merge_paragraph_spacing(
2734 inherited.as_ref().and_then(|value| value.spacing.as_ref()),
2735 style.spacing.as_ref(),
2736 ),
2737 indent: merge_paragraph_indent(
2738 inherited.as_ref().and_then(|value| value.indent.as_ref()),
2739 style.indent.as_ref(),
2740 ),
2741 background_color: merge_paragraph_background_color(
2742 inherited
2743 .as_ref()
2744 .and_then(|value| value.background_color.as_deref()),
2745 style.background_color.as_deref(),
2746 ),
2747 borders: merge_paragraph_border_sets(
2748 inherited.as_ref().and_then(|value| value.borders.as_ref()),
2749 style.borders.as_ref(),
2750 ),
2751 tab_stops: merge_paragraph_tab_stops(
2752 inherited
2753 .as_ref()
2754 .and_then(|value| value.tab_stops.as_deref()),
2755 style.tab_stops.as_deref(),
2756 ),
2757 contextual_spacing: merge_paragraph_boolean(
2758 inherited
2759 .as_ref()
2760 .and_then(|value| value.contextual_spacing),
2761 style.contextual_spacing,
2762 ),
2763 keep_next: merge_paragraph_boolean(
2764 inherited.as_ref().and_then(|value| value.keep_next),
2765 style.keep_next,
2766 ),
2767 keep_lines: merge_paragraph_boolean(
2768 inherited.as_ref().and_then(|value| value.keep_lines),
2769 style.keep_lines,
2770 ),
2771 widow_control: merge_paragraph_boolean(
2772 inherited.as_ref().and_then(|value| value.widow_control),
2773 style.widow_control,
2774 ),
2775 page_break_before: merge_paragraph_boolean(
2776 inherited.as_ref().and_then(|value| value.page_break_before),
2777 style.page_break_before,
2778 ),
2779 run_style: merge_text_styles(
2780 inherited.as_ref().and_then(|value| value.run_style.clone()),
2781 style.run_style.clone(),
2782 ),
2783 ui_priority: style
2784 .ui_priority
2785 .or_else(|| inherited.as_ref().and_then(|value| value.ui_priority)),
2786 is_default: style.is_default,
2787 is_primary: style.is_primary,
2788 };
2789
2790 paragraph_style_cache.insert(style_id.to_string(), Some(resolved.clone()));
2791 stack.remove(style_id);
2792 Some(resolved)
2793 }
2794
2795 let mut paragraph_styles: Vec<ParagraphStyleDefinition> = raw_styles_by_id
2796 .values()
2797 .filter(|style| style.style_type == RawStyleType::Paragraph)
2798 .filter_map(|style| {
2799 let mut stack = HashSet::new();
2800 resolve_paragraph_style(
2801 &style.id,
2802 &raw_styles_by_id,
2803 &mut paragraph_style_cache,
2804 &mut stack,
2805 )
2806 })
2807 .collect();
2808
2809 for style in &mut paragraph_styles {
2810 style.snap_to_grid = style.snap_to_grid.or(default_paragraph_style
2811 .as_ref()
2812 .and_then(|style| style.snap_to_grid));
2813 style.text_alignment = style.text_alignment.or(default_paragraph_style
2814 .as_ref()
2815 .and_then(|style| style.text_alignment));
2816 }
2817
2818 paragraph_styles.sort_by(|left, right| {
2819 let left_priority = left.ui_priority.unwrap_or(9999);
2820 let right_priority = right.ui_priority.unwrap_or(9999);
2821 if left_priority != right_priority {
2822 left_priority.cmp(&right_priority)
2823 } else {
2824 left.name.cmp(&right.name)
2825 }
2826 });
2827
2828 let paragraph_style_by_id = paragraph_styles
2829 .iter()
2830 .map(|style| (style.id.clone(), style.clone()))
2831 .collect::<HashMap<_, _>>();
2832
2833 let mut run_style_by_id = HashMap::new();
2834 for style_id in raw_styles_by_id.keys() {
2835 let mut stack = HashSet::new();
2836 if let Some(resolved) = resolve_run_style(
2837 style_id,
2838 &raw_styles_by_id,
2839 &mut run_style_cache,
2840 &mut stack,
2841 ) {
2842 run_style_by_id.insert(style_id.clone(), resolved);
2843 }
2844 }
2845
2846 let mut table_style_cache = HashMap::new();
2847 fn resolve_table_style(
2848 style_id: &str,
2849 raw_table_styles_by_id: &HashMap<String, ParsedTableStyleDefinition>,
2850 table_style_cache: &mut HashMap<String, Option<ParsedTableStyleDefinition>>,
2851 stack: &mut HashSet<String>,
2852 ) -> Option<ParsedTableStyleDefinition> {
2853 if let Some(cached) = table_style_cache.get(style_id) {
2854 return cached.to_owned();
2855 }
2856 if stack.contains(style_id) {
2857 return None;
2858 }
2859 stack.insert(style_id.to_string());
2860
2861 let style = raw_table_styles_by_id.get(style_id)?;
2862 let inherited = style.based_on_id.as_deref().and_then(|based_on_id| {
2863 resolve_table_style(
2864 based_on_id,
2865 raw_table_styles_by_id,
2866 table_style_cache,
2867 stack,
2868 )
2869 });
2870
2871 let mut conditions = HashMap::new();
2872 for conditional_type in TableConditionalStyleType::ALL {
2873 if let Some(merged_condition) = merge_table_conditional_style(
2874 inherited
2875 .as_ref()
2876 .and_then(|value| value.conditions.get(&conditional_type)),
2877 style.conditions.get(&conditional_type),
2878 ) {
2879 conditions.insert(conditional_type, merged_condition);
2880 }
2881 }
2882
2883 let resolved = ParsedTableStyleDefinition {
2884 id: style.id.clone(),
2885 based_on_id: style.based_on_id.clone(),
2886 name: style.name.clone(),
2887 conditions,
2888 floating: style.floating.clone(),
2889 properties: style.properties.clone(),
2890 has_row_cell_spacing: style.has_row_cell_spacing
2891 || inherited
2892 .as_ref()
2893 .is_some_and(|value| value.has_row_cell_spacing),
2894 };
2895
2896 table_style_cache.insert(style_id.to_string(), Some(resolved.clone()));
2897 stack.remove(style_id);
2898 Some(resolved)
2899 }
2900
2901 let mut table_style_by_id = HashMap::new();
2902 for style_id in raw_table_styles_by_id.keys() {
2903 let mut stack = HashSet::new();
2904 if let Some(resolved) = resolve_table_style(
2905 style_id,
2906 &raw_table_styles_by_id,
2907 &mut table_style_cache,
2908 &mut stack,
2909 ) {
2910 table_style_by_id.insert(style_id.clone(), resolved);
2911 }
2912 }
2913
2914 let default_paragraph_style_id = paragraph_styles
2915 .iter()
2916 .find(|style| style.is_default == Some(true))
2917 .map(|style| style.id.clone())
2918 .or_else(|| {
2919 if paragraph_style_by_id.contains_key("Normal") {
2920 Some("Normal".to_string())
2921 } else {
2922 None
2923 }
2924 });
2925
2926 let resolved_default_paragraph_style = default_paragraph_style_id
2927 .as_ref()
2928 .and_then(|style_id| paragraph_style_by_id.get(style_id).cloned());
2929
2930 let merged_default_paragraph_style =
2931 resolved_default_paragraph_style
2932 .as_ref()
2933 .map(|resolved| ParagraphStyle {
2934 source_before_auto_spacing: None,
2935 source_after_auto_spacing: None,
2936 align: resolved.align.or(default_paragraph_style
2937 .as_ref()
2938 .and_then(|value| value.align)),
2939 snap_to_grid: resolved.snap_to_grid.or(default_paragraph_style
2940 .as_ref()
2941 .and_then(|value| value.snap_to_grid)),
2942 source_snap_to_grid: None,
2943 source_has_snap_to_grid: None,
2944 source_inherited_snap_to_grid: None,
2945 source_table_snap_to_grid: None,
2946 text_alignment: resolved.text_alignment.or(default_paragraph_style
2947 .as_ref()
2948 .and_then(|value| value.text_alignment)),
2949 source_text_alignment: None,
2950 source_has_text_alignment: None,
2951 source_inherited_text_alignment: None,
2952 source_table_text_alignment: None,
2953 heading_level: resolved.heading_level,
2954 style_id: default_paragraph_style_id.clone(),
2955 style_name: Some(resolved.name.clone()),
2956 numbering: resolved.numbering.clone().or_else(|| {
2957 default_paragraph_style
2958 .as_ref()
2959 .and_then(|value| value.numbering.clone())
2960 }),
2961 spacing: merge_paragraph_spacing(
2962 default_paragraph_style
2963 .as_ref()
2964 .and_then(|value| value.spacing.as_ref()),
2965 resolved.spacing.as_ref(),
2966 ),
2967 indent: merge_paragraph_indent(
2968 default_paragraph_style
2969 .as_ref()
2970 .and_then(|value| value.indent.as_ref()),
2971 resolved.indent.as_ref(),
2972 ),
2973 background_color: merge_paragraph_background_color(
2974 default_paragraph_style
2975 .as_ref()
2976 .and_then(|value| value.background_color.as_deref()),
2977 resolved.background_color.as_deref(),
2978 ),
2979 borders: merge_paragraph_border_sets(
2980 default_paragraph_style
2981 .as_ref()
2982 .and_then(|value| value.borders.as_ref()),
2983 resolved.borders.as_ref(),
2984 ),
2985 tab_stops: merge_paragraph_tab_stops(
2986 default_paragraph_style
2987 .as_ref()
2988 .and_then(|value| value.tab_stops.as_deref()),
2989 resolved.tab_stops.as_deref(),
2990 ),
2991 contextual_spacing: merge_paragraph_boolean(
2992 default_paragraph_style
2993 .as_ref()
2994 .and_then(|value| value.contextual_spacing),
2995 resolved.contextual_spacing,
2996 ),
2997 keep_next: merge_paragraph_boolean(
2998 default_paragraph_style
2999 .as_ref()
3000 .and_then(|value| value.keep_next),
3001 resolved.keep_next,
3002 ),
3003 keep_lines: merge_paragraph_boolean(
3004 default_paragraph_style
3005 .as_ref()
3006 .and_then(|value| value.keep_lines),
3007 resolved.keep_lines,
3008 ),
3009 widow_control: merge_paragraph_boolean(
3010 default_paragraph_style
3011 .as_ref()
3012 .and_then(|value| value.widow_control),
3013 resolved.widow_control,
3014 ),
3015 page_break_before: merge_paragraph_boolean(
3016 default_paragraph_style
3017 .as_ref()
3018 .and_then(|value| value.page_break_before),
3019 resolved.page_break_before,
3020 ),
3021 drop_cap: None,
3022 });
3023
3024 let mut table_paragraph_spacing_by_style_id = HashMap::new();
3025 for (style_id, _) in raw_styles_by_id
3026 .iter()
3027 .filter(|(_, style)| style.style_type == RawStyleType::Table)
3028 {
3029 let mut chain = Vec::new();
3030 let mut cursor = Some(style_id.clone());
3031 let mut seen = HashSet::new();
3032 while let Some(id) = cursor {
3033 if !seen.insert(id.clone()) {
3034 break;
3035 }
3036 let Some(chain_style) = raw_styles_by_id.get(&id) else {
3037 break;
3038 };
3039 if chain_style.style_type != RawStyleType::Table {
3040 break;
3041 }
3042 chain.push(chain_style);
3043 cursor = chain_style.based_on_id.clone();
3044 }
3045 let mut merged_spacing: Option<crate::model::ParagraphSpacing> = None;
3046 for chain_style in chain.iter().rev() {
3047 merged_spacing = crate::parse::util::merge_paragraph_spacing(
3048 merged_spacing.as_ref(),
3049 chain_style.spacing.as_ref(),
3050 );
3051 }
3052 if let Some(spacing) = merged_spacing {
3053 table_paragraph_spacing_by_style_id.insert(style_id.clone(), spacing);
3054 }
3055 }
3056
3057 ParsedStyleSheet {
3058 paragraph_styles,
3059 paragraph_style_by_id,
3060 run_style_by_id,
3061 table_style_by_id,
3062 default_table_style_id,
3063 table_paragraph_spacing_by_style_id,
3064 doc_default_paragraph_spacing: default_paragraph_style
3065 .as_ref()
3066 .and_then(|style| style.spacing.clone()),
3067 default_paragraph_text_alignment: default_paragraph_style
3068 .as_ref()
3069 .and_then(|style| style.text_alignment),
3070 default_paragraph_style: merged_default_paragraph_style,
3071 default_paragraph_style_id,
3072 default_run_style: merge_text_styles(
3073 default_run_style,
3074 resolved_default_paragraph_style.and_then(|value| value.run_style),
3075 ),
3076 theme_colors,
3077 theme_fonts,
3078 }
3079}
3080
3081pub fn clone_paragraph_style_definition(
3082 style: &ParagraphStyleDefinition,
3083) -> ParagraphStyleDefinition {
3084 style.clone()
3085}
3086
3087pub fn clone_numbering_definitions(definitions: &NumberingDefinitionSet) -> NumberingDefinitionSet {
3088 definitions.clone()
3089}
3090
3091#[cfg(test)]
3092mod table_box_spacing_tests {
3093 use super::parse_table_box_spacing;
3094
3095 #[test]
3096 fn parses_start_end_aliases() {
3097 let xml =
3098 r#"<w:tcMar><w:start w:w="0" w:type="dxa"/><w:end w:w="0" w:type="dxa"/></w:tcMar>"#;
3099 let spacing = parse_table_box_spacing(xml).expect("spacing");
3100 assert_eq!(spacing.left_twips, Some(0));
3101 assert_eq!(spacing.right_twips, Some(0));
3102 assert_eq!(spacing.top_twips, None);
3103 assert_eq!(spacing.bottom_twips, None);
3104 }
3105}
3106
3107#[cfg(all(test, not(target_arch = "wasm32")))]
3108mod document_style_cache_tests {
3109 #[test]
3110 fn shared_definitions_track_references_theme_changes_and_eviction() {
3111 for selector in (0..12).chain((0..12).rev()) {
3112 let selected = if selector % 2 == 0 { "a" } else { "b" };
3113 let font = if selector % 3 == 0 { "Serif" } else { "Sans" };
3114 let bold = if selector % 4 == 0 { "0" } else { "1" };
3115 let mut package = crate::package::OoxmlPackage {
3116 parts: Default::default(),
3117 binary_assets: Default::default(),
3118 warnings: vec![],
3119 };
3120 for (name, content) in [
3121 ("word/document.xml", format!("<w:document><w:body><w:p><w:r><w:rPr><w:rStyle w:val='{selected}'/></w:rPr><w:t>text</w:t></w:r></w:p></w:body></w:document>")),
3122 ("word/styles.xml", format!("<w:styles><w:style w:type='paragraph' w:styleId='normal' w:default='1'><w:name w:val='Normal'/></w:style><w:style w:type='character' w:styleId='a'><w:rPr><w:b w:val='{bold}'/></w:rPr></w:style><w:style w:type='character' w:styleId='b'><w:rPr><w:i/></w:rPr></w:style></w:styles>")),
3123 ("word/theme/theme1.xml", format!("<a:theme><a:themeElements><a:fontScheme><a:majorFont><a:latin typeface='{font}'/></a:majorFont><a:minorFont><a:latin typeface='{font}'/></a:minorFont></a:fontScheme></a:themeElements></a:theme>")),
3124 ] { package.parts.insert(name.into(), crate::package::OoxmlPart { name: name.into(), content }); }
3125 let expected = super::parse_document_style_sheet(&package);
3126 let actual = super::acquire_document_style_sheet(&package);
3127 assert_eq!(&*actual, &expected);
3128 let (mut metadata, _) = super::release_document_style_metadata(actual);
3129 if let Some(style) = metadata.first_mut() {
3130 style.name = "Changed".into();
3131 }
3132 assert_eq!(&*super::acquire_document_style_sheet(&package), &expected);
3133 }
3134 }
3135}
3136
3137
3138#[cfg(all(test, not(target_arch = "wasm32")))]
3139mod bounded_large_definition_tests {
3140 use super::*;
3141
3142 #[test]
3143 fn large_definitions_preserve_values_and_resident_budget() {
3144 for value in ["first", "second", "third", "first"] {
3145 let mut pkg = crate::package::OoxmlPackage {
3146 parts: Default::default(), binary_assets: Default::default(), warnings: vec![],
3147 };
3148 let name = format!("{value}{}", "x".repeat(140 * 1024));
3149 let content = format!("<w:styles><w:style w:type='paragraph' w:styleId='normal' w:default='1'><w:name w:val='{name}'/></w:style></w:styles>");
3150 pkg.parts.insert("word/styles.xml".into(), crate::package::OoxmlPart { name: "word/styles.xml".into(), content });
3151 let expected = parse_document_style_sheet(&pkg);
3152 for _ in 0..2 {
3153 assert_eq!(*acquire_document_style_sheet(&pkg), expected);
3154 DOCUMENT_STYLE_CACHE.read(|cache| {
3155 assert!(cache.len() <= 4);
3156 assert!(cache.iter().map(|entry| entry.source_bytes).sum::<usize>() <= 512 * 1024);
3157 });
3158 }
3159 }
3160 }
3161}