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 text_shadow: None,
1141 text_outline: None,
1142 };
1143
1144 for style in [inherited, direct].into_iter().flatten() {
1145 if style.bold.is_some() {
1146 merged.bold = style.bold;
1147 }
1148 if style.italic.is_some() {
1149 merged.italic = style.italic;
1150 }
1151 if style.underline.is_some() {
1152 merged.underline = style.underline;
1153 }
1154 if style.strike.is_some() {
1155 merged.strike = style.strike;
1156 }
1157 if style.all_caps.is_some() {
1158 merged.all_caps = style.all_caps;
1159 }
1160 if style.small_caps.is_some() {
1161 merged.small_caps = style.small_caps;
1162 }
1163 if style.color.is_some() {
1164 merged.color = style.color;
1165 }
1166 if style.highlight.is_some() {
1167 merged.highlight = style.highlight;
1168 }
1169 if style.background_color.is_some() {
1170 merged.background_color = style.background_color;
1171 }
1172 if style.font_size_pt.is_some() {
1173 merged.font_size_pt = style.font_size_pt;
1174 }
1175 if style.font_size_cs_pt.is_some() {
1176 merged.font_size_cs_pt = style.font_size_cs_pt;
1177 }
1178 if style.font_family.is_some() {
1179 merged.font_family = style.font_family;
1180 }
1181 if style.source_font_family.is_some() {
1182 merged.source_font_family = style.source_font_family;
1183 }
1184 if style.font_family_ascii.is_some() {
1185 merged.font_family_ascii = style.font_family_ascii;
1186 }
1187 if style.font_family_h_ansi.is_some() {
1188 merged.font_family_h_ansi = style.font_family_h_ansi;
1189 }
1190 if style.font_family_east_asia.is_some() {
1191 merged.font_family_east_asia = style.font_family_east_asia;
1192 }
1193 if style.font_family_cs.is_some() {
1194 merged.font_family_cs = style.font_family_cs;
1195 }
1196 if style.font_theme_ascii.is_some() {
1197 merged.font_theme_ascii = style.font_theme_ascii;
1198 }
1199 if style.font_theme_h_ansi.is_some() {
1200 merged.font_theme_h_ansi = style.font_theme_h_ansi;
1201 }
1202 if style.font_theme_east_asia.is_some() {
1203 merged.font_theme_east_asia = style.font_theme_east_asia;
1204 }
1205 if style.font_theme_cs.is_some() {
1206 merged.font_theme_cs = style.font_theme_cs;
1207 }
1208 if style.resolved_font_family_ascii.is_some() {
1209 merged.resolved_font_family_ascii = style.resolved_font_family_ascii;
1210 }
1211 if style.resolved_font_family_h_ansi.is_some() {
1212 merged.resolved_font_family_h_ansi = style.resolved_font_family_h_ansi;
1213 }
1214 if style.resolved_font_family_east_asia.is_some() {
1215 merged.resolved_font_family_east_asia = style.resolved_font_family_east_asia;
1216 }
1217 if style.resolved_font_family_cs.is_some() {
1218 merged.resolved_font_family_cs = style.resolved_font_family_cs;
1219 }
1220 if style.font_hint.is_some() {
1221 merged.font_hint = style.font_hint;
1222 }
1223 if style.language.is_some() {
1224 merged.language = style.language;
1225 }
1226 if style.language_east_asia.is_some() {
1227 merged.language_east_asia = style.language_east_asia;
1228 }
1229 if style.language_bidi.is_some() {
1230 merged.language_bidi = style.language_bidi;
1231 }
1232 if style.right_to_left.is_some() {
1233 merged.right_to_left = style.right_to_left;
1234 }
1235 if style.complex_script.is_some() {
1236 merged.complex_script = style.complex_script;
1237 }
1238 if style.character_scale_percent.is_some() || style.source_character_scale.is_some() {
1239 merged.character_scale_percent = style.character_scale_percent;
1240 merged.source_character_scale = style.source_character_scale;
1241 }
1242 if style.character_spacing_twips.is_some() {
1243 merged.character_spacing_twips = style.character_spacing_twips;
1244 }
1245 if style.kerning_min_font_size_pt.is_some() {
1246 merged.kerning_min_font_size_pt = style.kerning_min_font_size_pt;
1247 }
1248 if style.ligatures.is_some() {
1249 merged.ligatures = style.ligatures;
1250 }
1251 if style.contextual_alternates.is_some() {
1252 merged.contextual_alternates = style.contextual_alternates;
1253 }
1254 if style.vertical_align.is_some() {
1255 merged.vertical_align = style.vertical_align;
1256 }
1257 if style.run_border.is_some() {
1258 merged.run_border = style.run_border;
1259 }
1260 if style.text_shadow.is_some() {
1261 merged.text_shadow = style.text_shadow;
1262 }
1263 if style.text_outline.is_some() {
1264 merged.text_outline = style.text_outline;
1265 }
1266 }
1267
1268 let has_any = merged.bold.is_some()
1269 || merged.italic.is_some()
1270 || merged.underline.is_some()
1271 || merged.strike.is_some()
1272 || merged.all_caps.is_some()
1273 || merged.small_caps.is_some()
1274 || merged.color.is_some()
1275 || merged.highlight.is_some()
1276 || merged.background_color.is_some()
1277 || merged.font_size_pt.is_some()
1278 || merged.font_size_cs_pt.is_some()
1279 || merged.font_family.is_some()
1280 || merged.source_font_family.is_some()
1281 || merged.font_family_ascii.is_some()
1282 || merged.font_family_h_ansi.is_some()
1283 || merged.font_family_east_asia.is_some()
1284 || merged.font_family_cs.is_some()
1285 || merged.font_theme_ascii.is_some()
1286 || merged.font_theme_h_ansi.is_some()
1287 || merged.font_theme_east_asia.is_some()
1288 || merged.font_theme_cs.is_some()
1289 || merged.resolved_font_family_ascii.is_some()
1290 || merged.resolved_font_family_h_ansi.is_some()
1291 || merged.resolved_font_family_east_asia.is_some()
1292 || merged.resolved_font_family_cs.is_some()
1293 || merged.font_hint.is_some()
1294 || merged.language.is_some()
1295 || merged.language_east_asia.is_some()
1296 || merged.language_bidi.is_some()
1297 || merged.right_to_left.is_some()
1298 || merged.complex_script.is_some()
1299 || merged.character_spacing_twips.is_some()
1300 || merged.character_scale_percent.is_some()
1301 || merged.source_character_scale.is_some()
1302 || merged.kerning_min_font_size_pt.is_some()
1303 || merged.ligatures.is_some()
1304 || merged.contextual_alternates.is_some()
1305 || merged.vertical_align.is_some()
1306 || merged.run_border.is_some()
1307 || merged.text_shadow.is_some()
1308 || merged.text_outline.is_some();
1309
1310 if has_any {
1311 Some(merged)
1312 } else {
1313 None
1314 }
1315}
1316
1317fn merge_paragraph_spacing(
1318 inherited: Option<&ParagraphSpacing>,
1319 direct: Option<&ParagraphSpacing>,
1320) -> Option<ParagraphSpacing> {
1321 if inherited.is_none() && direct.is_none() {
1322 return None;
1323 }
1324
1325 let merged = ParagraphSpacing {
1326 before_twips: direct
1327 .and_then(|value| value.before_twips)
1328 .or_else(|| inherited.and_then(|value| value.before_twips)),
1329 after_twips: direct
1330 .and_then(|value| value.after_twips)
1331 .or_else(|| inherited.and_then(|value| value.after_twips)),
1332 before_auto_spacing: direct
1333 .and_then(|value| value.before_auto_spacing)
1334 .or_else(|| inherited.and_then(|value| value.before_auto_spacing)),
1335 after_auto_spacing: direct
1336 .and_then(|value| value.after_auto_spacing)
1337 .or_else(|| inherited.and_then(|value| value.after_auto_spacing)),
1338 line_twips: direct
1339 .and_then(|value| value.line_twips)
1340 .or_else(|| inherited.and_then(|value| value.line_twips)),
1341 line_rule: direct
1342 .and_then(|value| value.line_rule)
1343 .or_else(|| inherited.and_then(|value| value.line_rule)),
1344 };
1345
1346 if merged.before_twips.is_none()
1347 && merged.after_twips.is_none()
1348 && merged.before_auto_spacing.is_none()
1349 && merged.after_auto_spacing.is_none()
1350 && merged.line_twips.is_none()
1351 && merged.line_rule.is_none()
1352 {
1353 return None;
1354 }
1355
1356 Some(merged)
1357}
1358
1359fn merge_paragraph_indent(
1360 inherited: Option<&ParagraphIndent>,
1361 direct: Option<&ParagraphIndent>,
1362) -> Option<ParagraphIndent> {
1363 if inherited.is_none() && direct.is_none() {
1364 return None;
1365 }
1366
1367 let merged = ParagraphIndent {
1368 left_twips: direct
1369 .and_then(|value| value.left_twips)
1370 .or_else(|| inherited.and_then(|value| value.left_twips)),
1371 right_twips: direct
1372 .and_then(|value| value.right_twips)
1373 .or_else(|| inherited.and_then(|value| value.right_twips)),
1374 first_line_twips: direct
1375 .and_then(|value| value.first_line_twips)
1376 .or_else(|| inherited.and_then(|value| value.first_line_twips)),
1377 hanging_twips: direct
1378 .and_then(|value| value.hanging_twips)
1379 .or_else(|| inherited.and_then(|value| value.hanging_twips)),
1380 };
1381
1382 if merged.left_twips.is_none()
1383 && merged.right_twips.is_none()
1384 && merged.first_line_twips.is_none()
1385 && merged.hanging_twips.is_none()
1386 {
1387 return None;
1388 }
1389
1390 Some(merged)
1391}
1392
1393fn merge_paragraph_background_color(
1394 inherited: Option<&str>,
1395 direct: Option<&str>,
1396) -> Option<String> {
1397 direct
1398 .map(str::to_string)
1399 .or_else(|| inherited.map(str::to_string))
1400}
1401
1402fn merge_paragraph_border_style(
1403 inherited: Option<&ParagraphBorderStyle>,
1404 direct: Option<&ParagraphBorderStyle>,
1405) -> Option<ParagraphBorderStyle> {
1406 if inherited.is_none() && direct.is_none() {
1407 return None;
1408 }
1409
1410 let border_type = direct
1411 .map(|value| value.border_type.clone())
1412 .or_else(|| inherited.map(|value| value.border_type.clone()))?;
1413 if border_type.is_empty() {
1414 return None;
1415 }
1416
1417 Some(ParagraphBorderStyle {
1418 border_type,
1419 color: direct
1420 .and_then(|value| value.color.clone())
1421 .or_else(|| inherited.and_then(|value| value.color.clone())),
1422 size_eighth_pt: direct
1423 .and_then(|value| value.size_eighth_pt)
1424 .or_else(|| inherited.and_then(|value| value.size_eighth_pt)),
1425 space_pt: direct
1426 .and_then(|value| value.space_pt)
1427 .or_else(|| inherited.and_then(|value| value.space_pt)),
1428 frame: direct
1429 .and_then(|value| value.frame)
1430 .or_else(|| inherited.and_then(|value| value.frame)),
1431 shadow: direct
1432 .and_then(|value| value.shadow)
1433 .or_else(|| inherited.and_then(|value| value.shadow)),
1434 })
1435}
1436
1437fn merge_paragraph_border_sets(
1438 inherited: Option<&ParagraphBorderSet>,
1439 direct: Option<&ParagraphBorderSet>,
1440) -> Option<ParagraphBorderSet> {
1441 if inherited.is_none() && direct.is_none() {
1442 return None;
1443 }
1444
1445 let merged = ParagraphBorderSet {
1446 top: merge_paragraph_border_style(
1447 inherited.and_then(|value| value.top.as_ref()),
1448 direct.and_then(|value| value.top.as_ref()),
1449 ),
1450 right: merge_paragraph_border_style(
1451 inherited.and_then(|value| value.right.as_ref()),
1452 direct.and_then(|value| value.right.as_ref()),
1453 ),
1454 bottom: merge_paragraph_border_style(
1455 inherited.and_then(|value| value.bottom.as_ref()),
1456 direct.and_then(|value| value.bottom.as_ref()),
1457 ),
1458 left: merge_paragraph_border_style(
1459 inherited.and_then(|value| value.left.as_ref()),
1460 direct.and_then(|value| value.left.as_ref()),
1461 ),
1462 between: merge_paragraph_border_style(
1463 inherited.and_then(|value| value.between.as_ref()),
1464 direct.and_then(|value| value.between.as_ref()),
1465 ),
1466 bar: merge_paragraph_border_style(
1467 inherited.and_then(|value| value.bar.as_ref()),
1468 direct.and_then(|value| value.bar.as_ref()),
1469 ),
1470 };
1471
1472 if merged.top.is_none()
1473 && merged.right.is_none()
1474 && merged.bottom.is_none()
1475 && merged.left.is_none()
1476 && merged.between.is_none()
1477 && merged.bar.is_none()
1478 {
1479 return None;
1480 }
1481
1482 Some(merged)
1483}
1484
1485fn merge_paragraph_tab_stops(
1486 inherited: Option<&[ParagraphTabStop]>,
1487 direct: Option<&[ParagraphTabStop]>,
1488) -> Option<Vec<ParagraphTabStop>> {
1489 if inherited.is_none() && direct.is_none() {
1490 return None;
1491 }
1492
1493 let mut by_position = HashMap::new();
1494 for stop in inherited.unwrap_or(&[]) {
1495 if let Some(position) = stop.position_twips {
1496 by_position.insert(position, stop.clone());
1497 }
1498 }
1499 for stop in direct.unwrap_or(&[]) {
1500 if let Some(position) = stop.position_twips {
1501 by_position.insert(position, stop.clone());
1502 }
1503 }
1504
1505 if by_position.is_empty() {
1506 return None;
1507 }
1508
1509 let mut combined: Vec<ParagraphTabStop> = by_position.into_values().collect();
1510 combined.sort_by_key(|stop| stop.position_twips.unwrap_or(0));
1511 Some(combined)
1512}
1513
1514fn merge_paragraph_boolean(inherited: Option<bool>, direct: Option<bool>) -> Option<bool> {
1515 if direct.is_some() {
1516 direct
1517 } else {
1518 inherited
1519 }
1520}
1521
1522fn resolve_theme_font(theme_token: Option<&str>, theme_fonts: &ThemeFontMap) -> Option<String> {
1523 let normalized = theme_token?.to_ascii_lowercase();
1524 if normalized.starts_with("major") {
1525 if normalized.contains("eastasia") {
1526 return theme_fonts
1527 .major_east_asia
1528 .clone()
1529 .or_else(|| theme_fonts.major_latin.clone());
1530 }
1531 if normalized.contains("bidi") || normalized.contains("cs") {
1532 return theme_fonts
1533 .major_complex_script
1534 .clone()
1535 .or_else(|| theme_fonts.major_latin.clone());
1536 }
1537 return theme_fonts.major_latin.clone();
1538 }
1539 if normalized.starts_with("minor") {
1540 if normalized.contains("eastasia") {
1541 return theme_fonts
1542 .minor_east_asia
1543 .clone()
1544 .or_else(|| theme_fonts.minor_latin.clone());
1545 }
1546 if normalized.contains("bidi") || normalized.contains("cs") {
1547 return theme_fonts
1548 .minor_complex_script
1549 .clone()
1550 .or_else(|| theme_fonts.minor_latin.clone());
1551 }
1552 return theme_fonts.minor_latin.clone();
1553 }
1554 None
1555}
1556
1557fn find_drawing_rpr_attribute(xml: &str, attribute: &str) -> Option<String> {
1558 let tag = super::scan::find_tag_token_ref(xml, "a:rPr")?;
1559 get_attribute(&tag, attribute)
1560}
1561
1562fn find_drawing_latin_typeface(xml: &str) -> Option<String> {
1563 if let Some(block) = crate::xml::extract_balanced_tag_slices(xml, "a:rPr")
1564 .into_iter()
1565 .next()
1566 {
1567 if let Some(latin) = crate::xml::extract_balanced_tag_slices(&block, "a:latin")
1568 .into_iter()
1569 .next()
1570 .or_else(|| super::scan::find_tag_token_ref(&block, "a:latin"))
1571 {
1572 if let Some(typeface) = get_attribute(&latin, "typeface") {
1573 return Some(typeface);
1574 }
1575 }
1576 }
1577
1578 if let Some(block) = crate::xml::extract_balanced_tag_slices(xml, "a:defRPr")
1579 .into_iter()
1580 .next()
1581 {
1582 if let Some(latin) = crate::xml::extract_balanced_tag_slices(&block, "a:latin")
1583 .into_iter()
1584 .next()
1585 .or_else(|| super::scan::find_tag_token_ref(&block, "a:latin"))
1586 {
1587 if let Some(typeface) = get_attribute(&latin, "typeface") {
1588 return Some(typeface);
1589 }
1590 }
1591 }
1592
1593 super::scan::find_tag_token_ref(xml, "a:latin")
1594 .and_then(|latin| get_attribute(&latin, "typeface"))
1595}
1596
1597fn find_drawing_color(xml: &str) -> Option<String> {
1598 if let Some(block) = crate::xml::extract_balanced_tag_slices(xml, "a:rPr")
1599 .into_iter()
1600 .next()
1601 {
1602 if let Some(color_xml) = crate::xml::extract_balanced_tag_slices(&block, "a:solidFill")
1603 .into_iter()
1604 .next()
1605 .or_else(|| super::scan::find_tag_token_ref(&block, "a:solidFill"))
1606 {
1607 if let Some(srgb) = super::scan::find_tag_token_ref(&color_xml, "a:srgbClr") {
1608 if let Some(color) = get_attribute(&srgb, "val")
1609 .as_deref()
1610 .and_then(|value| normalize_hex_color(Some(value)))
1611 {
1612 return Some(color);
1613 }
1614 }
1615 }
1616 if let Some(srgb) = super::scan::find_tag_token_ref(&block, "a:srgbClr") {
1617 if let Some(color) = get_attribute(&srgb, "val")
1618 .as_deref()
1619 .and_then(|value| normalize_hex_color(Some(value)))
1620 {
1621 return Some(color);
1622 }
1623 }
1624 }
1625 None
1626}
1627
1628fn contains_east_asia_glyphs(text: &str) -> bool {
1629 text.chars().any(|ch| {
1630 matches!(ch,
1631 '\u{2e80}'..='\u{9fff}'
1632 | '\u{ac00}'..='\u{d7af}'
1633 )
1634 })
1635}
1636
1637fn contains_complex_script_glyphs(text: &str) -> bool {
1638 text.chars().any(|ch| {
1639 matches!(ch,
1640 '\u{0590}'..='\u{08ff}'
1641 | '\u{fb1d}'..='\u{fefc}'
1642 )
1643 })
1644}
1645
1646fn collect_decoded_text_samples(xml: &str) -> String {
1647 let mut samples = String::new();
1648 for tag_name in ["w:t", "a:t"] {
1649 for block in crate::xml::extract_balanced_tag_slices(xml, tag_name) {
1650 if let Some(start) = block.find('>') {
1651 let end = block.rfind("</").unwrap_or(block.len());
1652 if start + 1 < end {
1653 samples.push_str(&decode_xml_entities(&block[start + 1..end]));
1654 }
1655 }
1656 }
1657 }
1658 samples
1659}
1660
1661pub fn parse_text_style_from_xml(xml: &str, theme_fonts: &ThemeFontMap) -> Option<TextStyle> {
1663 super::style::cached_definition_style(xml, theme_fonts)
1664}
1665
1666pub(crate) fn parse_definition_style_uncached(
1667 xml: &str,
1668 theme_fonts: &ThemeFontMap,
1669) -> Option<TextStyle> {
1670 if xml.is_empty() {
1671 return None;
1672 }
1673
1674 let bold = parse_on_off_attribute(xml, "b");
1675 let italic = parse_on_off_attribute(xml, "i");
1676 let underline = parse_underline_attribute(xml);
1677 let strike = parse_on_off_attribute(xml, "strike");
1678 let all_caps = parse_on_off_attribute(xml, "caps");
1679 let small_caps = parse_on_off_attribute(xml, "smallCaps");
1680
1681 let color_match = super::scan::find_attribute_value_in_tag(xml, "w:color", "w:val");
1682 let highlight_match = super::scan::find_attribute_value_in_tag(xml, "w:highlight", "w:val");
1683 let shading_tag = super::scan::find_tag_token_ref(xml, "w:shd");
1684 let character_spacing_match =
1685 super::scan::find_attribute_value_in_tag(xml, "w:spacing", "w:val");
1686 let kerning_match = super::scan::find_attribute_value_in_tag(xml, "w:kern", "w:val");
1687 let size_match = super::scan::find_attribute_value_in_tag(xml, "w:sz", "w:val");
1688 let complex_size_match = super::scan::find_attribute_value_in_tag(xml, "w:szCs", "w:val");
1689 let run_fonts_tag = super::scan::find_tag_token_ref(xml, "w:rFonts").unwrap_or_default();
1690 let ascii_font = get_attribute(&run_fonts_tag, "w:ascii");
1691 let h_ansi_font = get_attribute(&run_fonts_tag, "w:hAnsi");
1692 let east_asia_font = get_attribute(&run_fonts_tag, "w:eastAsia");
1693 let complex_script_font = get_attribute(&run_fonts_tag, "w:cs");
1694 let ascii_theme_font = get_attribute(&run_fonts_tag, "w:asciiTheme");
1695 let h_ansi_theme_font = get_attribute(&run_fonts_tag, "w:hAnsiTheme");
1696 let east_asia_theme_font = get_attribute(&run_fonts_tag, "w:eastAsiaTheme");
1697 let complex_script_theme_font = get_attribute(&run_fonts_tag, "w:csTheme");
1698 let font_hint = get_attribute(&run_fonts_tag, "w:hint");
1699 let language_tag = super::scan::find_tag_token_ref(xml, "w:lang").unwrap_or_default();
1700 let language = get_attribute(&language_tag, "w:val");
1701 let language_east_asia = get_attribute(&language_tag, "w:eastAsia");
1702 let language_bidi = get_attribute(&language_tag, "w:bidi");
1703 let right_to_left = parse_on_off_attribute(xml, "rtl");
1704 let complex_script = parse_on_off_attribute(xml, "cs");
1705 let vertical_align_match =
1706 super::scan::find_attribute_value_in_tag(xml, "w:vertAlign", "w:val");
1707 let drawing_bold_match = find_drawing_rpr_attribute(xml, "b");
1708 let drawing_italic_match = find_drawing_rpr_attribute(xml, "i");
1709 let drawing_underline_match = find_drawing_rpr_attribute(xml, "u");
1710 let drawing_strike_match =
1711 find_drawing_rpr_attribute(xml, "strike").or_else(|| find_drawing_rpr_attribute(xml, "s"));
1712 let drawing_color_match = find_drawing_color(xml);
1713 let drawing_size_match = find_drawing_rpr_attribute(xml, "sz");
1714 let drawing_font_match = find_drawing_latin_typeface(xml);
1715 let run_border_tag = super::scan::find_tag_token_ref(xml, "w:bdr");
1716 let decoded_text_samples = collect_decoded_text_samples(xml);
1717 let contains_east_asia = contains_east_asia_glyphs(&decoded_text_samples);
1718 let contains_complex_script = contains_complex_script_glyphs(&decoded_text_samples);
1719
1720 let mut style = TextStyle {
1721 bold: None,
1722 italic: None,
1723 underline: None,
1724 strike: None,
1725 all_caps: None,
1726 small_caps: None,
1727 color: None,
1728 highlight: None,
1729 background_color: None,
1730 font_size_pt: None,
1731 font_size_cs_pt: None,
1732 font_family: None,
1733 source_font_family: None,
1734 font_family_ascii: None,
1735 font_family_h_ansi: None,
1736 font_family_east_asia: None,
1737 font_family_cs: None,
1738 font_theme_ascii: None,
1739 font_theme_h_ansi: None,
1740 font_theme_east_asia: None,
1741 font_theme_cs: None,
1742 resolved_font_family_ascii: None,
1743 resolved_font_family_h_ansi: None,
1744 resolved_font_family_east_asia: None,
1745 resolved_font_family_cs: None,
1746 font_hint: None,
1747 language: None,
1748 language_east_asia: None,
1749 language_bidi: None,
1750 right_to_left: None,
1751 complex_script: None,
1752 character_spacing_twips: None,
1753 character_scale_percent: None,
1754 source_character_scale: None,
1755 kerning_min_font_size_pt: None,
1756 ligatures: super::style::parse_text_ligatures_from_xml(xml),
1757 contextual_alternates: super::style::parse_contextual_alternates_from_xml(xml),
1758 vertical_align: None,
1759 run_border: None,
1760 text_shadow: None,
1761 text_outline: None,
1762 };
1763
1764 style.character_scale_percent = super::style::parse_character_scale_percent_from_xml(xml);
1765 style.source_character_scale = super::scan::find_attribute_value_in_tag(xml, "w:w", "w:val");
1766 style.bold = bold;
1767 style.italic = italic;
1768 style.underline = underline;
1769 style.strike = strike;
1770 style.all_caps = all_caps;
1771 style.small_caps = small_caps;
1772
1773 if style.bold.is_none() {
1774 if let Some(value) = drawing_bold_match {
1775 let value = value.to_ascii_lowercase();
1776 style.bold = Some(value != "0" && value != "false");
1777 }
1778 }
1779 if style.italic.is_none() {
1780 if let Some(value) = drawing_italic_match {
1781 let value = value.to_ascii_lowercase();
1782 style.italic = Some(value != "0" && value != "false");
1783 }
1784 }
1785 if style.underline.is_none() {
1786 if let Some(value) = drawing_underline_match {
1787 let value = value.to_ascii_lowercase();
1788 style.underline = Some(value != "none" && value != "false" && value != "0");
1789 }
1790 }
1791 if style.strike.is_none() {
1792 if let Some(value) = drawing_strike_match {
1793 let value = value.to_ascii_lowercase();
1794 style.strike =
1795 Some(value != "nostrike" && value != "none" && value != "false" && value != "0");
1796 }
1797 }
1798
1799 if let Some(color) = normalize_hex_color(color_match.as_deref()) {
1800 style.color = Some(color);
1801 } else if let Some(color) = drawing_color_match {
1802 style.color = Some(color);
1803 }
1804
1805 if let Some(highlight) = highlight_match {
1806 style.highlight = Some(highlight);
1807 }
1808
1809 if let Some(shading_tag) = shading_tag {
1810 if let Some(shading_fill) =
1811 normalize_hex_color(get_attribute(&shading_tag, "w:fill").as_deref())
1812 {
1813 style.background_color = Some(shading_fill);
1814 }
1815 }
1816
1817 if let Some(character_spacing) = character_spacing_match.and_then(|raw| raw.parse::<i64>().ok())
1818 {
1819 style.character_spacing_twips = Some(character_spacing);
1820 }
1821
1822 if let Some(kerning_raw) = kerning_match.and_then(|raw| raw.parse::<f64>().ok()) {
1823 if kerning_raw.is_finite() && kerning_raw >= 0.0 {
1824 style.kerning_min_font_size_pt = Some(kerning_raw / 2.0);
1825 }
1826 }
1827
1828 if let Some(size_raw) = size_match.and_then(|raw| raw.parse::<f64>().ok()) {
1829 style.font_size_pt = Some(size_raw / 2.0);
1830 } else if let Some(size_raw) = drawing_size_match.and_then(|raw| raw.parse::<f64>().ok()) {
1831 style.font_size_pt = Some(size_raw / 100.0);
1832 }
1833 if let Some(size_raw) = complex_size_match.and_then(|raw| raw.parse::<f64>().ok()) {
1834 style.font_size_cs_pt = Some(size_raw / 2.0);
1835 }
1836
1837 style.font_family_ascii = ascii_font.clone();
1838 style.font_family_h_ansi = h_ansi_font.clone();
1839 style.font_family_east_asia = east_asia_font.clone();
1840 style.font_family_cs = complex_script_font.clone();
1841 style.font_theme_ascii = ascii_theme_font.clone();
1842 style.font_theme_h_ansi = h_ansi_theme_font.clone();
1843 style.font_theme_east_asia = east_asia_theme_font.clone();
1844 style.font_theme_cs = complex_script_theme_font.clone();
1845 style.resolved_font_family_ascii = ascii_theme_font
1846 .as_deref()
1847 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
1848 .or_else(|| ascii_font.clone());
1849 style.resolved_font_family_h_ansi = h_ansi_theme_font
1850 .as_deref()
1851 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
1852 .or_else(|| h_ansi_font.clone());
1853 style.resolved_font_family_east_asia = east_asia_theme_font
1854 .as_deref()
1855 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
1856 .or_else(|| east_asia_font.clone());
1857 style.resolved_font_family_cs = complex_script_theme_font
1858 .as_deref()
1859 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
1860 .or_else(|| complex_script_font.clone());
1861 style.font_hint = font_hint;
1862 style.language = language;
1863 style.language_east_asia = language_east_asia;
1864 style.language_bidi = language_bidi;
1865 style.right_to_left = right_to_left;
1866 style.complex_script = complex_script;
1867
1868 let run_font_family = ascii_font.clone().or(h_ansi_font.clone());
1869 let run_theme_font_token = ascii_theme_font.clone().or(h_ansi_theme_font.clone());
1870 let east_asia_fallback_font = if contains_east_asia {
1871 east_asia_font.clone()
1872 } else {
1873 None
1874 };
1875 let east_asia_fallback_theme_token = if contains_east_asia {
1876 east_asia_theme_font.clone()
1877 } else {
1878 None
1879 };
1880 let complex_script_fallback_font = if contains_complex_script {
1881 complex_script_font.clone()
1882 } else {
1883 None
1884 };
1885 let complex_script_fallback_theme_token = if contains_complex_script {
1886 complex_script_theme_font.clone()
1887 } else {
1888 None
1889 };
1890 let symbol_fallback_font = {
1891 let candidate = east_asia_font.clone().or(complex_script_font.clone());
1892 candidate.filter(|font| {
1893 let lower = font.to_ascii_lowercase();
1894 lower.contains("symbol")
1895 || lower.contains("emoji")
1896 || lower.contains("dingbat")
1897 || lower.contains("wingding")
1898 || lower.contains("webding")
1899 })
1900 };
1901
1902 if let Some(font_family) = run_font_family {
1903 style.font_family = Some(font_family);
1904 } else if let Some(theme_token) = run_theme_font_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) = east_asia_fallback_font {
1909 style.font_family = Some(font_family);
1910 } else if let Some(theme_token) = east_asia_fallback_theme_token.as_deref() {
1911 if let Some(font_family) = resolve_theme_font(Some(theme_token), theme_fonts) {
1912 style.font_family = Some(font_family);
1913 }
1914 } else if let Some(font_family) = complex_script_fallback_font {
1915 style.font_family = Some(font_family);
1916 } else if let Some(theme_token) = complex_script_fallback_theme_token.as_deref() {
1917 if let Some(font_family) = resolve_theme_font(Some(theme_token), theme_fonts) {
1918 style.font_family = Some(font_family);
1919 }
1920 } else if let Some(font_family) = symbol_fallback_font {
1921 style.font_family = Some(font_family);
1922 } else if let Some(font_family) = drawing_font_match {
1923 style.font_family = Some(font_family);
1924 }
1925 style.source_font_family = style.font_family.clone();
1926
1927 if let Some(vertical_align_value) = vertical_align_match.map(|value| value.to_ascii_lowercase())
1928 {
1929 style.vertical_align = match vertical_align_value.as_str() {
1930 "superscript" => Some(VerticalAlign::Superscript),
1931 "subscript" => Some(VerticalAlign::Subscript),
1932 _ => None,
1933 };
1934 }
1935
1936 style.run_border = parse_text_run_border_style(run_border_tag.as_deref());
1937 super::text_effects::parse_into(&mut style, xml);
1938
1939 merge_text_styles(None, Some(style))
1940}
1941
1942pub fn parse_paragraph_align_from_xml(xml: &str) -> Option<ParagraphAlignment> {
1944 map_alignment(super::scan::find_attribute_value_in_tag(xml, "w:jc", "w:val").as_deref())
1945}
1946
1947pub(crate) fn parse_heading_level_from_outline(xml: &str) -> Option<Option<HeadingLevel>> {
1948 let tag = direct_table_property(xml, "w:outlineLvl")?;
1949 let outline_value = get_attribute(tag, "w:val")?;
1950 let level = outline_value.parse::<i64>().ok()? + 1;
1951 Some(match level {
1952 1 => Some(HeadingLevel::One),
1953 2 => Some(HeadingLevel::Two),
1954 3 => Some(HeadingLevel::Three),
1955 4 => Some(HeadingLevel::Four),
1956 5 => Some(HeadingLevel::Five),
1957 6 => Some(HeadingLevel::Six),
1958 7 => Some(HeadingLevel::Seven),
1959 8 => Some(HeadingLevel::Eight),
1960 9 => Some(HeadingLevel::Nine),
1961 _ => None,
1962 })
1963}
1964
1965fn parse_style_type(value: Option<&str>) -> Option<RawStyleType> {
1966 match value?.to_ascii_lowercase().as_str() {
1967 "paragraph" => Some(RawStyleType::Paragraph),
1968 "character" => Some(RawStyleType::Character),
1969 "numbering" => Some(RawStyleType::Numbering),
1970 "table" => Some(RawStyleType::Table),
1971 _ => None,
1972 }
1973}
1974
1975fn resolve_style_properties_block<'a>(style_xml: &'a str, tag_name: &str) -> &'a str {
1976 crate::xml::extract_balanced_tag_slices(style_xml, tag_name)
1977 .into_iter()
1978 .next()
1979 .or_else(|| super::scan::find_tag_token_ref(style_xml, tag_name))
1980 .unwrap_or_default()
1981}
1982
1983fn strip_table_conditional_style_blocks(style_xml: &str) -> String {
1984 if style_xml.is_empty() {
1985 return style_xml.to_string();
1986 }
1987
1988 let mut output = style_xml.to_string();
1989 loop {
1990 let before_len = output.len();
1991 output = remove_balanced_tag_blocks(&output, "w:tblStylePr");
1992 output = remove_self_closing_tags(&output, "w:tblStylePr");
1993 if output.len() == before_len {
1994 break;
1995 }
1996 }
1997 output
1998}
1999
2000fn remove_balanced_tag_blocks(xml: &str, tag_name: &str) -> String {
2001 let mut output = String::new();
2002 let mut last_index = 0;
2003 for range in extract_balanced_tag_ranges(xml, tag_name) {
2004 output.push_str(&xml[last_index..range.start]);
2005 last_index = range.end;
2006 }
2007 output.push_str(&xml[last_index..]);
2008 output
2009}
2010
2011fn remove_self_closing_tags(xml: &str, tag_name: &str) -> String {
2012 let mut output = String::new();
2013 let mut last_index = 0;
2014 let open_prefix = format!("<{tag_name}");
2015 let mut index = 0;
2016 while index < xml.len() {
2017 if xml.as_bytes().get(index) != Some(&b'<')
2018 || !super::scan::starts_with_ignore_ascii_case(&xml[index..], &open_prefix)
2019 {
2020 index += 1;
2021 continue;
2022 }
2023
2024 let after_prefix = index + open_prefix.len();
2025 let mut cursor = after_prefix;
2026 let bytes = xml.as_bytes();
2027 while cursor < bytes.len() {
2028 if bytes[cursor] == b'>' {
2029 let token = &xml[index..=cursor];
2030 if token.ends_with("/>") || token.ends_with("/ >") {
2031 output.push_str(&xml[last_index..index]);
2032 last_index = cursor + 1;
2033 }
2034 index = cursor + 1;
2035 break;
2036 }
2037 cursor += 1;
2038 }
2039 if cursor >= bytes.len() {
2040 break;
2041 }
2042 }
2043 output.push_str(&xml[last_index..]);
2044 output
2045}
2046
2047fn extract_balanced_tag_ranges(xml: &str, tag_name: &str) -> Vec<crate::xml::TagRange> {
2048 crate::xml::extract_balanced_tag_ranges(xml, tag_name)
2049}
2050
2051fn read_typeface(xml: &str, tag_name: &str) -> Option<String> {
2052 let token = super::scan::find_tag_token_ref(xml, &format!("a:{tag_name}"))?;
2053 let value = get_attribute(&token, "typeface")?.trim().to_string();
2054 if value.is_empty() {
2055 None
2056 } else {
2057 Some(value)
2058 }
2059}
2060
2061pub fn parse_theme_fonts(pkg: &OoxmlPackage) -> ThemeFontMap {
2063 let Some(theme_xml) =
2064 crate::package::related_document_part(pkg, "theme").map(|part| part.content.as_str())
2065 else {
2066 return ThemeFontMap::default();
2067 };
2068
2069 let major_font_xml = crate::xml::extract_balanced_tag_slices(theme_xml, "a:majorFont")
2070 .into_iter()
2071 .next()
2072 .unwrap_or_default();
2073 let minor_font_xml = crate::xml::extract_balanced_tag_slices(theme_xml, "a:minorFont")
2074 .into_iter()
2075 .next()
2076 .unwrap_or_default();
2077
2078 ThemeFontMap {
2079 major_latin: read_typeface(&major_font_xml, "latin"),
2080 major_east_asia: read_typeface(&major_font_xml, "ea"),
2081 major_complex_script: read_typeface(&major_font_xml, "cs"),
2082 minor_latin: read_typeface(&minor_font_xml, "latin"),
2083 minor_east_asia: read_typeface(&minor_font_xml, "ea"),
2084 minor_complex_script: read_typeface(&minor_font_xml, "cs"),
2085 }
2086}
2087
2088fn parse_theme_color_from_tag(color_xml: &str) -> Option<String> {
2089 if let Some(explicit) = super::scan::find_attribute_value_in_tag(color_xml, "a:srgbClr", "val")
2090 .as_deref()
2091 .and_then(|value| normalize_hex_color(Some(value)))
2092 {
2093 return Some(explicit);
2094 }
2095
2096 super::scan::find_attribute_value_in_tag(color_xml, "a:sysClr", "lastClr")
2097 .as_deref()
2098 .and_then(|value| normalize_hex_color(Some(value)))
2099}
2100
2101pub fn parse_theme_colors(pkg: &OoxmlPackage) -> ThemeColorMap {
2103 let Some(theme_xml) =
2104 crate::package::related_document_part(pkg, "theme").map(|part| part.content.as_str())
2105 else {
2106 return ThemeColorMap::new();
2107 };
2108
2109 let color_scheme_xml = crate::xml::extract_balanced_tag_slices(theme_xml, "a:clrScheme")
2110 .into_iter()
2111 .next()
2112 .unwrap_or_default();
2113 if color_scheme_xml.is_empty() {
2114 return ThemeColorMap::new();
2115 }
2116
2117 let color_names = [
2118 "dk1",
2119 "lt1",
2120 "dk2",
2121 "lt2",
2122 "accent1",
2123 "accent2",
2124 "accent3",
2125 "accent4",
2126 "accent5",
2127 "accent6",
2128 "hlink",
2129 "folHlink",
2130 "bg1",
2131 "bg2",
2132 "tx1",
2133 "tx2",
2134 "followedHyperlink",
2135 ];
2136
2137 let mut colors = ThemeColorMap::new();
2138 for name in color_names {
2139 let tag_match =
2140 crate::xml::extract_balanced_tag_slices(&color_scheme_xml, &format!("a:{name}"))
2141 .into_iter()
2142 .next()
2143 .or_else(|| {
2144 super::scan::find_tag_token_ref(&color_scheme_xml, &format!("a:{name}"))
2145 });
2146 let Some(tag_match) = tag_match else {
2147 continue;
2148 };
2149 if let Some(color) = parse_theme_color_from_tag(&tag_match) {
2150 colors.insert(name.to_ascii_lowercase(), color);
2151 }
2152 }
2153
2154 colors
2155}
2156
2157const WORD_DECLARED_DEFAULT_FONT_SIZE_PT: f64 = 10.0;
2159
2160fn with_word_application_defaults(doc_defaults_xml: &str, theme_fonts: &ThemeFontMap) -> String {
2166 let declares_run_defaults = super::scan::contains_tag(doc_defaults_xml, "w:rPrDefault");
2167 let declares_paragraph_defaults = super::scan::contains_tag(doc_defaults_xml, "w:pPrDefault");
2168 if declares_run_defaults && declares_paragraph_defaults {
2169 return doc_defaults_xml.to_string();
2170 }
2171 let mut supplied = String::new();
2172 if !declares_run_defaults {
2173 let fonts = if theme_fonts.minor_latin.is_some() {
2174 r#"<w:rFonts w:asciiTheme="minorHAnsi" w:eastAsiaTheme="minorEastAsia" w:hAnsiTheme="minorHAnsi" w:cstheme="minorBidi"/>"#
2175 } else {
2176 r#"<w:rFonts w:ascii="Aptos" w:hAnsi="Aptos"/>"#
2177 };
2178 supplied.push_str(&format!(
2179 r#"<w:rPrDefault><w:rPr>{fonts}<w:sz w:val="24"/><w:szCs w:val="24"/></w:rPr></w:rPrDefault>"#
2180 ));
2181 }
2182 if !declares_paragraph_defaults {
2183 supplied.push_str(
2184 r#"<w:pPrDefault><w:pPr><w:spacing w:after="160" w:line="276" w:lineRule="auto"/></w:pPr></w:pPrDefault>"#,
2185 );
2186 }
2187 match doc_defaults_xml.rfind("</w:docDefaults>") {
2188 Some(end) => format!(
2189 "{}{}{}",
2190 &doc_defaults_xml[..end],
2191 supplied,
2192 &doc_defaults_xml[end..]
2193 ),
2194 None => format!("<w:docDefaults>{supplied}</w:docDefaults>"),
2195 }
2196}
2197
2198pub fn parse_style_sheet(pkg: &OoxmlPackage) -> ParsedStyleSheet {
2200 parse_style_sheet_selected(pkg, None)
2201}
2202
2203pub(crate) fn parse_document_style_sheet(pkg: &OoxmlPackage) -> ParsedStyleSheet {
2204 parse_referenced_style_sheet(pkg, document_style_references(pkg))
2205}
2206
2207fn document_style_references(pkg: &OoxmlPackage) -> HashSet<String> {
2208 let mut referenced = HashSet::new();
2209 for part in pkg.parts.values() {
2210 for name in ["w:tblStyle", "w:rStyle", "w:pStyle"] {
2211 for tag in crate::xml::extract_balanced_tag_slices(&part.content, name) {
2212 if let Some(id) = get_attribute(tag, "w:val") {
2213 referenced.insert(id);
2214 }
2215 }
2216 }
2217 }
2218 referenced
2219}
2220
2221fn parse_referenced_style_sheet(
2222 pkg: &OoxmlPackage,
2223 mut referenced: HashSet<String>,
2224) -> ParsedStyleSheet {
2225 if let Some(part) = crate::package::related_document_part(pkg, "styles") {
2226 let mut parents = HashMap::new();
2227 for style in crate::xml::extract_balanced_tag_slices(&part.content, "w:style") {
2228 let tag = super::scan::find_tag_token_ref(style, "w:style").unwrap_or("");
2229 let Some(id) = get_attribute(tag, "w:styleId") else {
2230 continue;
2231 };
2232 if let Some(parent) =
2233 super::scan::find_attribute_value_in_tag(style, "w:basedOn", "w:val")
2234 {
2235 parents.insert(id.clone(), parent);
2236 }
2237 let kind = parse_style_type(get_attribute(tag, "w:type").as_deref());
2238 if kind == Some(RawStyleType::Paragraph)
2239 || (kind == Some(RawStyleType::Table)
2240 && get_attribute(tag, "w:default")
2241 .is_some_and(|v| matches!(v.as_str(), "1" | "true" | "on")))
2242 {
2243 referenced.insert(id);
2244 }
2245 }
2246 let mut pending: Vec<_> = referenced.iter().cloned().collect();
2247 while let Some(id) = pending.pop() {
2248 if let Some(parent) = parents.get(&id) {
2249 if referenced.insert(parent.clone()) {
2250 pending.push(parent.clone());
2251 }
2252 }
2253 }
2254 }
2255 parse_style_sheet_selected(pkg, Some(&referenced))
2256}
2257
2258#[cfg(not(target_arch = "wasm32"))]
2259pub(crate) type DocumentStyleSheet = std::sync::Arc<ParsedStyleSheet>;
2260#[cfg(target_arch = "wasm32")]
2261pub(crate) type DocumentStyleSheet = ParsedStyleSheet;
2262
2263#[cfg(not(target_arch = "wasm32"))]
2264struct CachedDocumentStyleSheet {
2265 styles: Option<String>,
2266 theme: Option<String>,
2267 references: Vec<String>,
2268 source_bytes: usize,
2269 parsed: std::sync::Arc<ParsedStyleSheet>,
2270}
2271
2272#[cfg(not(target_arch = "wasm32"))]
2273static DOCUMENT_STYLE_CACHE: std::sync::LazyLock<
2274 super::cache::Snapshot<std::collections::VecDeque<std::sync::Arc<CachedDocumentStyleSheet>>>,
2275> = std::sync::LazyLock::new(|| super::cache::Snapshot::new(std::collections::VecDeque::new()));
2276
2277#[cfg(not(target_arch = "wasm32"))]
2278pub(crate) fn cached_document_style_identity(styles: &ParsedStyleSheet) -> Option<DocumentStyleSheet> {
2279 DOCUMENT_STYLE_CACHE.read(|cache| cache.iter()
2280 .find(|entry| std::ptr::eq(entry.parsed.as_ref(), styles))
2281 .map(|entry| std::sync::Arc::clone(&entry.parsed)))
2282}
2283
2284pub(crate) fn acquire_document_style_sheet(pkg: &OoxmlPackage) -> DocumentStyleSheet {
2285 #[cfg(target_arch = "wasm32")]
2286 {
2287 parse_document_style_sheet(pkg)
2288 }
2289 #[cfg(not(target_arch = "wasm32"))]
2290 {
2291 let styles =
2292 crate::package::related_document_part(pkg, "styles").map(|part| part.content.as_str());
2293 let theme =
2294 crate::package::related_document_part(pkg, "theme").map(|part| part.content.as_str());
2295 if styles
2296 .map_or(0, str::len)
2297 .saturating_add(theme.map_or(0, str::len))
2298 > 512 * 1024
2299 {
2300 return std::sync::Arc::new(parse_document_style_sheet(pkg));
2301 }
2302 let references = document_style_references(pkg);
2303 if references.len() > 512 || references.iter().map(String::len).sum::<usize>() > 32 * 1024 {
2304 return std::sync::Arc::new(parse_referenced_style_sheet(pkg, references));
2305 }
2306 let mut references: Vec<_> = references.into_iter().collect();
2307 references.sort_unstable();
2308 let source_bytes = styles.map_or(0, str::len)
2309 + theme.map_or(0, str::len)
2310 + references.iter().map(String::len).sum::<usize>();
2311 let matches = |entry: &CachedDocumentStyleSheet| {
2312 entry.styles.as_deref() == styles
2313 && entry.theme.as_deref() == theme
2314 && entry.references == references
2315 };
2316 if let Some(parsed) = DOCUMENT_STYLE_CACHE.read(|cache| cache.iter()
2317 .find(|entry| matches(entry)).map(|entry| entry.parsed.clone())) {
2318 return parsed;
2319 }
2320 let parsed = std::sync::Arc::new(parse_referenced_style_sheet(
2321 pkg, references.iter().cloned().collect(),
2322 ));
2323 if source_bytes > 512 * 1024 { return parsed; }
2324 let entry = std::sync::Arc::new(CachedDocumentStyleSheet {
2325 styles: styles.map(str::to_owned),
2326 theme: theme.map(str::to_owned),
2327 references,
2328 source_bytes,
2329 parsed: parsed.clone(),
2330 });
2331 DOCUMENT_STYLE_CACHE.publish(move |cache| {
2332 if cache.iter().any(|existing| existing.styles == entry.styles
2333 && existing.theme == entry.theme && existing.references == entry.references) {
2334 return cache.clone();
2335 }
2336 let mut next = cache.clone();
2337 let mut retained = next.iter().map(|entry| entry.source_bytes).sum::<usize>();
2338 while next.len() >= 4 || retained + entry.source_bytes > 512 * 1024 {
2339 let Some(old) = next.pop_front() else { break };
2340 retained -= old.source_bytes;
2341 }
2342 next.push_back(entry);
2343 next
2344 });
2345 parsed
2346 }
2347}
2348
2349pub(crate) fn release_document_style_metadata(
2350 styles: DocumentStyleSheet,
2351) -> (Vec<crate::model::ParagraphStyleDefinition>, Option<String>) {
2352 #[cfg(not(target_arch = "wasm32"))]
2353 let styles = match std::sync::Arc::try_unwrap(styles) {
2354 Ok(styles) => styles,
2355 Err(styles) => {
2356 return (
2357 styles.paragraph_styles.clone(),
2358 styles.default_paragraph_style_id.clone(),
2359 )
2360 }
2361 };
2362 (styles.paragraph_styles, styles.default_paragraph_style_id)
2363}
2364
2365fn parse_style_sheet_selected(
2366 pkg: &OoxmlPackage,
2367 selected_styles: Option<&HashSet<String>>,
2368) -> ParsedStyleSheet {
2369 let Some(styles_xml) =
2370 crate::package::related_document_part(pkg, "styles").map(|part| part.content.as_str())
2371 else {
2372 return empty_style_sheet();
2373 };
2374
2375 let theme_fonts = parse_theme_fonts(pkg);
2376 let theme_colors = parse_theme_colors(pkg);
2377 let declared_doc_defaults_xml =
2378 crate::xml::extract_balanced_tag_slices(styles_xml, "w:docDefaults")
2379 .into_iter()
2380 .next()
2381 .unwrap_or_default();
2382 let declares_run_defaults =
2383 super::scan::contains_tag(&declared_doc_defaults_xml, "w:rPrDefault");
2384 let doc_defaults_xml = with_word_application_defaults(&declared_doc_defaults_xml, &theme_fonts);
2385
2386 let default_paragraph_style = {
2387 let paragraph_defaults = resolve_style_properties_block(&doc_defaults_xml, "w:pPr");
2388 let default_paragraph_has_num_pr =
2389 super::scan::contains_tag(¶graph_defaults, "w:numPr");
2390 let align = parse_paragraph_align_from_xml(¶graph_defaults);
2391 let snap_to_grid = parse_on_off_attribute(¶graph_defaults, "snapToGrid");
2392 let text_alignment = super::style::parse_paragraph_text_alignment_from_xml(
2393 direct_table_property(&doc_defaults_xml, "w:pPrDefault")
2394 .and_then(|xml| direct_table_property(xml, "w:pPr"))
2395 .unwrap_or_default(),
2396 );
2397 let spacing = parse_paragraph_spacing_from_xml(¶graph_defaults);
2398 let indent = parse_paragraph_indent_from_xml(¶graph_defaults);
2399 let background_color = parse_paragraph_shading_from_xml(¶graph_defaults);
2400 let borders = parse_paragraph_border_set_from_xml(¶graph_defaults);
2401 let tab_stops = parse_paragraph_tab_stops_from_xml(¶graph_defaults);
2402 let parsed_default_numbering = parse_paragraph_numbering_from_xml(¶graph_defaults);
2403 let numbering = if default_paragraph_has_num_pr && parsed_default_numbering.is_none() {
2404 Some(ParagraphNumbering { num_id: 0, ilvl: 0 })
2405 } else {
2406 parsed_default_numbering
2407 };
2408 let contextual_spacing = parse_on_off_attribute(¶graph_defaults, "contextualSpacing");
2409 let keep_next = parse_on_off_attribute(¶graph_defaults, "keepNext");
2410 let keep_lines = parse_on_off_attribute(¶graph_defaults, "keepLines");
2411 let widow_control = parse_on_off_attribute(¶graph_defaults, "widowControl");
2412 let page_break_before = parse_on_off_attribute(¶graph_defaults, "pageBreakBefore");
2413
2414 if align.is_none()
2415 && snap_to_grid.is_none()
2416 && text_alignment.is_none()
2417 && spacing.is_none()
2418 && indent.is_none()
2419 && background_color.is_none()
2420 && borders.is_none()
2421 && tab_stops.is_empty()
2422 && numbering.is_none()
2423 && contextual_spacing.is_none()
2424 && keep_next.is_none()
2425 && keep_lines.is_none()
2426 && widow_control.is_none()
2427 && page_break_before.is_none()
2428 {
2429 None
2430 } else {
2431 Some(ParagraphStyle {
2432 source_before_auto_spacing: None,
2433 source_after_auto_spacing: None,
2434 align,
2435 snap_to_grid,
2436 source_snap_to_grid: None,
2437 source_has_snap_to_grid: None,
2438 source_inherited_snap_to_grid: None,
2439 source_table_snap_to_grid: None,
2440 text_alignment,
2441 source_text_alignment: None,
2442 source_has_text_alignment: None,
2443 source_inherited_text_alignment: None,
2444 source_table_text_alignment: None,
2445 heading_level: None,
2446 style_id: None,
2447 style_name: None,
2448 numbering,
2449 spacing,
2450 indent,
2451 background_color,
2452 borders,
2453 tab_stops: if tab_stops.is_empty() {
2454 None
2455 } else {
2456 Some(tab_stops)
2457 },
2458 contextual_spacing,
2459 keep_next,
2460 keep_lines,
2461 widow_control,
2462 page_break_before,
2463 drop_cap: None,
2464 })
2465 }
2466 };
2467
2468 let mut default_run_style = parse_text_style_from_xml(
2469 &resolve_style_properties_block(&doc_defaults_xml, "w:rPr"),
2470 &theme_fonts,
2471 );
2472 if declares_run_defaults {
2473 match default_run_style.as_mut() {
2475 Some(style) => {
2476 if style.font_size_pt.is_none() {
2477 style.font_size_pt = Some(WORD_DECLARED_DEFAULT_FONT_SIZE_PT);
2478 }
2479 if style.font_size_cs_pt.is_none() {
2480 style.font_size_cs_pt = Some(WORD_DECLARED_DEFAULT_FONT_SIZE_PT);
2481 }
2482 }
2483 None => {
2484 default_run_style = parse_text_style_from_xml(
2485 r#"<w:rPr><w:sz w:val="20"/><w:szCs w:val="20"/></w:rPr>"#,
2486 &theme_fonts,
2487 );
2488 }
2489 }
2490 }
2491
2492 let mut raw_styles_by_id = HashMap::new();
2493 let mut raw_table_styles_by_id = HashMap::new();
2494 let mut default_table_style_id = None;
2495
2496 for style_xml in crate::xml::extract_balanced_tag_slices(styles_xml, "w:style") {
2497 let style_tag = super::scan::find_tag_token_ref(&style_xml, "w:style").unwrap_or_default();
2498 let style_id = get_attribute(&style_tag, "w:styleId");
2499 let style_type = parse_style_type(get_attribute(&style_tag, "w:type").as_deref());
2500 let (Some(style_id), Some(style_type)) = (style_id, style_type) else {
2501 continue;
2502 };
2503 if selected_styles.is_some_and(|ids| !ids.contains(&style_id)) {
2504 continue;
2505 }
2506
2507 let name_tag = super::scan::find_tag_token_ref(&style_xml, "w:name").unwrap_or_default();
2508 let based_on_tag =
2509 super::scan::find_tag_token_ref(&style_xml, "w:basedOn").unwrap_or_default();
2510 let next_tag = super::scan::find_tag_token_ref(&style_xml, "w:next").unwrap_or_default();
2511 let ui_priority_tag =
2512 super::scan::find_tag_token_ref(&style_xml, "w:uiPriority").unwrap_or_default();
2513 let paragraph_properties_xml = resolve_style_properties_block(&style_xml, "w:pPr");
2514 let style_has_paragraph_num_pr =
2515 super::scan::contains_tag(¶graph_properties_xml, "w:numPr");
2516 let run_properties_xml = resolve_style_properties_block(&style_xml, "w:rPr");
2517
2518 let heading_level = normalize_heading_level(Some(&style_id))
2519 .or_else(|| normalize_heading_level(get_attribute(&name_tag, "w:val").as_deref()));
2520 let outline_heading = parse_heading_level_from_outline(¶graph_properties_xml);
2521
2522 let parsed_style_numbering = parse_paragraph_numbering_from_xml(¶graph_properties_xml);
2523 let tab_stops = parse_paragraph_tab_stops_from_xml(¶graph_properties_xml);
2524
2525 raw_styles_by_id.insert(
2526 style_id.clone(),
2527 RawStyleDefinition {
2528 id: style_id.clone(),
2529 style_type,
2530 name: get_attribute(&name_tag, "w:val").unwrap_or(style_id.clone()),
2531 based_on_id: get_attribute(&based_on_tag, "w:val"),
2532 next_style_id: get_attribute(&next_tag, "w:val"),
2533 align: parse_paragraph_align_from_xml(¶graph_properties_xml),
2534 snap_to_grid: parse_on_off_attribute(¶graph_properties_xml, "snapToGrid"),
2535 text_alignment: super::style::parse_paragraph_text_alignment_from_xml(
2536 direct_table_property(&style_xml, "w:pPr").unwrap_or_default(),
2537 ),
2538 heading_level,
2539 outline_heading,
2540 numbering: if style_has_paragraph_num_pr && parsed_style_numbering.is_none() {
2541 Some(ParagraphNumbering { num_id: 0, ilvl: 0 })
2542 } else {
2543 parsed_style_numbering
2544 },
2545 spacing: parse_paragraph_spacing_from_xml(¶graph_properties_xml),
2546 indent: parse_paragraph_indent_from_xml(¶graph_properties_xml),
2547 background_color: parse_paragraph_shading_from_xml(¶graph_properties_xml),
2548 borders: parse_paragraph_border_set_from_xml(¶graph_properties_xml),
2549 tab_stops: if tab_stops.is_empty() {
2550 None
2551 } else {
2552 Some(tab_stops)
2553 },
2554 contextual_spacing: parse_on_off_attribute(
2555 ¶graph_properties_xml,
2556 "contextualSpacing",
2557 ),
2558 keep_next: parse_on_off_attribute(¶graph_properties_xml, "keepNext"),
2559 keep_lines: parse_on_off_attribute(¶graph_properties_xml, "keepLines"),
2560 widow_control: parse_on_off_attribute(¶graph_properties_xml, "widowControl"),
2561 page_break_before: parse_on_off_attribute(
2562 ¶graph_properties_xml,
2563 "pageBreakBefore",
2564 ),
2565 run_style: parse_text_style_from_xml(&run_properties_xml, &theme_fonts),
2566 ui_priority: get_attribute(&ui_priority_tag, "w:val")
2567 .and_then(|raw| raw.parse::<f64>().ok())
2568 .filter(|value| value.is_finite())
2569 .map(|value| value.round() as i64),
2570 is_default: get_attribute(&style_tag, "w:default")
2571 .is_some_and(|value| value == "1")
2572 .then_some(true),
2573 is_primary: super::scan::contains_tag(&style_xml, "w:qFormat").then_some(true),
2574 },
2575 );
2576
2577 if style_type == RawStyleType::Table
2578 && selected_styles.is_none_or(|ids| ids.contains(&style_id))
2579 {
2580 if get_attribute(&style_tag, "w:default")
2581 .is_some_and(|value| matches!(value.as_str(), "1" | "true" | "on"))
2582 {
2583 default_table_style_id = Some(style_id.clone());
2584 }
2585 let mut conditions = HashMap::new();
2586 let whole_table_source_xml = strip_table_conditional_style_blocks(&style_xml);
2587 if let Some(whole_table_condition) =
2588 parse_table_conditional_style_from_xml(&whole_table_source_xml, &theme_fonts)
2589 {
2590 conditions.insert(TableConditionalStyleType::WholeTable, whole_table_condition);
2591 }
2592
2593 for conditional_style_xml in
2594 crate::xml::extract_balanced_tag_slices(&style_xml, "w:tblStylePr")
2595 {
2596 let conditional_style_tag =
2597 super::scan::find_tag_token_ref(&conditional_style_xml, "w:tblStylePr")
2598 .unwrap_or_default();
2599 let Some(conditional_type) = normalize_table_conditional_style_type(
2600 get_attribute(&conditional_style_tag, "w:type").as_deref(),
2601 ) else {
2602 continue;
2603 };
2604 if let Some(condition) =
2605 parse_table_conditional_style_from_xml(&conditional_style_xml, &theme_fonts)
2606 {
2607 conditions.insert(conditional_type, condition);
2608 }
2609 }
2610
2611 raw_table_styles_by_id.insert(
2612 style_id.clone(),
2613 ParsedTableStyleDefinition {
2614 id: style_id.clone(),
2615 based_on_id: get_attribute(&based_on_tag, "w:val"),
2616 name: get_attribute(&name_tag, "w:val").unwrap_or(style_id),
2617 conditions,
2618 floating: None,
2619 properties: None,
2620 has_row_cell_spacing: crate::xml::extract_balanced_tag_slices(
2621 &style_xml, "w:trPr",
2622 )
2623 .into_iter()
2624 .any(|properties| {
2625 direct_table_property(properties, "w:tblCellSpacing").is_some()
2626 }),
2627 },
2628 );
2629 }
2630 }
2631
2632 let mut run_style_cache = HashMap::new();
2633 fn resolve_run_style(
2634 style_id: &str,
2635 raw_styles_by_id: &HashMap<String, RawStyleDefinition>,
2636 run_style_cache: &mut HashMap<String, Option<TextStyle>>,
2637 stack: &mut HashSet<String>,
2638 ) -> Option<TextStyle> {
2639 if let Some(cached) = run_style_cache.get(style_id) {
2640 return cached.to_owned();
2641 }
2642 if stack.contains(style_id) {
2643 return None;
2644 }
2645 stack.insert(style_id.to_string());
2646
2647 let style = raw_styles_by_id.get(style_id)?;
2648 let inherited = style.based_on_id.as_deref().and_then(|based_on_id| {
2649 resolve_run_style(based_on_id, raw_styles_by_id, run_style_cache, stack)
2650 });
2651 let resolved = merge_text_styles(inherited, style.run_style.clone());
2652 run_style_cache.insert(style_id.to_string(), resolved.clone());
2653 stack.remove(style_id);
2654 resolved
2655 }
2656
2657 let mut paragraph_style_cache = HashMap::new();
2658 fn resolve_paragraph_style(
2659 style_id: &str,
2660 raw_styles_by_id: &HashMap<String, RawStyleDefinition>,
2661 paragraph_style_cache: &mut HashMap<String, Option<ParagraphStyleDefinition>>,
2662 stack: &mut HashSet<String>,
2663 ) -> Option<ParagraphStyleDefinition> {
2664 if let Some(cached) = paragraph_style_cache.get(style_id) {
2665 return cached.to_owned();
2666 }
2667 if stack.contains(style_id) {
2668 return None;
2669 }
2670 stack.insert(style_id.to_string());
2671
2672 let style = raw_styles_by_id.get(style_id)?;
2673 if style.style_type != RawStyleType::Paragraph {
2674 paragraph_style_cache.insert(style_id.to_string(), None);
2675 stack.remove(style_id);
2676 return None;
2677 }
2678
2679 let inherited = style.based_on_id.as_deref().and_then(|based_on_id| {
2680 raw_styles_by_id
2681 .get(based_on_id)
2682 .filter(|parent| parent.style_type == RawStyleType::Paragraph)
2683 .and_then(|_| {
2684 resolve_paragraph_style(
2685 based_on_id,
2686 raw_styles_by_id,
2687 paragraph_style_cache,
2688 stack,
2689 )
2690 })
2691 });
2692
2693 let mut outline_style = Some(style);
2694 let mut outline_seen = HashSet::new();
2695 let mut outline_heading = None;
2696 while let Some(current) = outline_style {
2697 if !outline_seen.insert(current.id.as_str()) {
2698 break;
2699 }
2700 if current.outline_heading.is_some() {
2701 outline_heading = current.outline_heading;
2702 break;
2703 }
2704 outline_style = current
2705 .based_on_id
2706 .as_deref()
2707 .and_then(|id| raw_styles_by_id.get(id))
2708 .filter(|parent| parent.style_type == RawStyleType::Paragraph);
2709 }
2710
2711 let resolved = ParagraphStyleDefinition {
2712 id: style.id.clone(),
2713 name: style.name.clone(),
2714 based_on_id: style.based_on_id.clone(),
2715 next_style_id: style.next_style_id.clone(),
2716 align: style
2717 .align
2718 .or(inherited.as_ref().and_then(|value| value.align)),
2719 snap_to_grid: style
2720 .snap_to_grid
2721 .or(inherited.as_ref().and_then(|value| value.snap_to_grid)),
2722 source_has_snap_to_grid: Some(
2723 style
2724 .snap_to_grid
2725 .or(inherited.as_ref().and_then(|value| value.snap_to_grid))
2726 .is_some(),
2727 ),
2728 text_alignment: style
2729 .text_alignment
2730 .or(inherited.as_ref().and_then(|value| value.text_alignment)),
2731 source_has_text_alignment: Some(
2732 style
2733 .text_alignment
2734 .or(inherited.as_ref().and_then(|value| value.text_alignment))
2735 .is_some(),
2736 ),
2737 heading_level: outline_heading.unwrap_or_else(|| {
2738 style
2739 .heading_level
2740 .or(inherited.as_ref().and_then(|value| value.heading_level))
2741 }),
2742 numbering: style
2743 .numbering
2744 .clone()
2745 .or_else(|| inherited.as_ref().and_then(|value| value.numbering.clone())),
2746 spacing: merge_paragraph_spacing(
2747 inherited.as_ref().and_then(|value| value.spacing.as_ref()),
2748 style.spacing.as_ref(),
2749 ),
2750 indent: merge_paragraph_indent(
2751 inherited.as_ref().and_then(|value| value.indent.as_ref()),
2752 style.indent.as_ref(),
2753 ),
2754 background_color: merge_paragraph_background_color(
2755 inherited
2756 .as_ref()
2757 .and_then(|value| value.background_color.as_deref()),
2758 style.background_color.as_deref(),
2759 ),
2760 borders: merge_paragraph_border_sets(
2761 inherited.as_ref().and_then(|value| value.borders.as_ref()),
2762 style.borders.as_ref(),
2763 ),
2764 tab_stops: merge_paragraph_tab_stops(
2765 inherited
2766 .as_ref()
2767 .and_then(|value| value.tab_stops.as_deref()),
2768 style.tab_stops.as_deref(),
2769 ),
2770 contextual_spacing: merge_paragraph_boolean(
2771 inherited
2772 .as_ref()
2773 .and_then(|value| value.contextual_spacing),
2774 style.contextual_spacing,
2775 ),
2776 keep_next: merge_paragraph_boolean(
2777 inherited.as_ref().and_then(|value| value.keep_next),
2778 style.keep_next,
2779 ),
2780 keep_lines: merge_paragraph_boolean(
2781 inherited.as_ref().and_then(|value| value.keep_lines),
2782 style.keep_lines,
2783 ),
2784 widow_control: merge_paragraph_boolean(
2785 inherited.as_ref().and_then(|value| value.widow_control),
2786 style.widow_control,
2787 ),
2788 page_break_before: merge_paragraph_boolean(
2789 inherited.as_ref().and_then(|value| value.page_break_before),
2790 style.page_break_before,
2791 ),
2792 run_style: merge_text_styles(
2793 inherited.as_ref().and_then(|value| value.run_style.clone()),
2794 style.run_style.clone(),
2795 ),
2796 ui_priority: style
2797 .ui_priority
2798 .or_else(|| inherited.as_ref().and_then(|value| value.ui_priority)),
2799 is_default: style.is_default,
2800 is_primary: style.is_primary,
2801 };
2802
2803 paragraph_style_cache.insert(style_id.to_string(), Some(resolved.clone()));
2804 stack.remove(style_id);
2805 Some(resolved)
2806 }
2807
2808 let mut paragraph_styles: Vec<ParagraphStyleDefinition> = raw_styles_by_id
2809 .values()
2810 .filter(|style| style.style_type == RawStyleType::Paragraph)
2811 .filter_map(|style| {
2812 let mut stack = HashSet::new();
2813 resolve_paragraph_style(
2814 &style.id,
2815 &raw_styles_by_id,
2816 &mut paragraph_style_cache,
2817 &mut stack,
2818 )
2819 })
2820 .collect();
2821
2822 for style in &mut paragraph_styles {
2823 style.snap_to_grid = style.snap_to_grid.or(default_paragraph_style
2824 .as_ref()
2825 .and_then(|style| style.snap_to_grid));
2826 style.text_alignment = style.text_alignment.or(default_paragraph_style
2827 .as_ref()
2828 .and_then(|style| style.text_alignment));
2829 }
2830
2831 paragraph_styles.sort_by(|left, right| {
2832 let left_priority = left.ui_priority.unwrap_or(9999);
2833 let right_priority = right.ui_priority.unwrap_or(9999);
2834 if left_priority != right_priority {
2835 left_priority.cmp(&right_priority)
2836 } else {
2837 left.name.cmp(&right.name)
2838 }
2839 });
2840
2841 let paragraph_style_by_id = paragraph_styles
2842 .iter()
2843 .map(|style| (style.id.clone(), style.clone()))
2844 .collect::<HashMap<_, _>>();
2845
2846 let mut run_style_by_id = HashMap::new();
2847 for style_id in raw_styles_by_id.keys() {
2848 let mut stack = HashSet::new();
2849 if let Some(resolved) = resolve_run_style(
2850 style_id,
2851 &raw_styles_by_id,
2852 &mut run_style_cache,
2853 &mut stack,
2854 ) {
2855 run_style_by_id.insert(style_id.clone(), resolved);
2856 }
2857 }
2858
2859 let mut table_style_cache = HashMap::new();
2860 fn resolve_table_style(
2861 style_id: &str,
2862 raw_table_styles_by_id: &HashMap<String, ParsedTableStyleDefinition>,
2863 table_style_cache: &mut HashMap<String, Option<ParsedTableStyleDefinition>>,
2864 stack: &mut HashSet<String>,
2865 ) -> Option<ParsedTableStyleDefinition> {
2866 if let Some(cached) = table_style_cache.get(style_id) {
2867 return cached.to_owned();
2868 }
2869 if stack.contains(style_id) {
2870 return None;
2871 }
2872 stack.insert(style_id.to_string());
2873
2874 let style = raw_table_styles_by_id.get(style_id)?;
2875 let inherited = style.based_on_id.as_deref().and_then(|based_on_id| {
2876 resolve_table_style(
2877 based_on_id,
2878 raw_table_styles_by_id,
2879 table_style_cache,
2880 stack,
2881 )
2882 });
2883
2884 let mut conditions = HashMap::new();
2885 for conditional_type in TableConditionalStyleType::ALL {
2886 if let Some(merged_condition) = merge_table_conditional_style(
2887 inherited
2888 .as_ref()
2889 .and_then(|value| value.conditions.get(&conditional_type)),
2890 style.conditions.get(&conditional_type),
2891 ) {
2892 conditions.insert(conditional_type, merged_condition);
2893 }
2894 }
2895
2896 let resolved = ParsedTableStyleDefinition {
2897 id: style.id.clone(),
2898 based_on_id: style.based_on_id.clone(),
2899 name: style.name.clone(),
2900 conditions,
2901 floating: style.floating.clone(),
2902 properties: style.properties.clone(),
2903 has_row_cell_spacing: style.has_row_cell_spacing
2904 || inherited
2905 .as_ref()
2906 .is_some_and(|value| value.has_row_cell_spacing),
2907 };
2908
2909 table_style_cache.insert(style_id.to_string(), Some(resolved.clone()));
2910 stack.remove(style_id);
2911 Some(resolved)
2912 }
2913
2914 let mut table_style_by_id = HashMap::new();
2915 for style_id in raw_table_styles_by_id.keys() {
2916 let mut stack = HashSet::new();
2917 if let Some(resolved) = resolve_table_style(
2918 style_id,
2919 &raw_table_styles_by_id,
2920 &mut table_style_cache,
2921 &mut stack,
2922 ) {
2923 table_style_by_id.insert(style_id.clone(), resolved);
2924 }
2925 }
2926
2927 let default_paragraph_style_id = paragraph_styles
2928 .iter()
2929 .find(|style| style.is_default == Some(true))
2930 .map(|style| style.id.clone())
2931 .or_else(|| {
2932 if paragraph_style_by_id.contains_key("Normal") {
2933 Some("Normal".to_string())
2934 } else {
2935 None
2936 }
2937 });
2938
2939 let resolved_default_paragraph_style = default_paragraph_style_id
2940 .as_ref()
2941 .and_then(|style_id| paragraph_style_by_id.get(style_id).cloned());
2942
2943 let merged_default_paragraph_style =
2944 resolved_default_paragraph_style
2945 .as_ref()
2946 .map(|resolved| ParagraphStyle {
2947 source_before_auto_spacing: None,
2948 source_after_auto_spacing: None,
2949 align: resolved.align.or(default_paragraph_style
2950 .as_ref()
2951 .and_then(|value| value.align)),
2952 snap_to_grid: resolved.snap_to_grid.or(default_paragraph_style
2953 .as_ref()
2954 .and_then(|value| value.snap_to_grid)),
2955 source_snap_to_grid: None,
2956 source_has_snap_to_grid: None,
2957 source_inherited_snap_to_grid: None,
2958 source_table_snap_to_grid: None,
2959 text_alignment: resolved.text_alignment.or(default_paragraph_style
2960 .as_ref()
2961 .and_then(|value| value.text_alignment)),
2962 source_text_alignment: None,
2963 source_has_text_alignment: None,
2964 source_inherited_text_alignment: None,
2965 source_table_text_alignment: None,
2966 heading_level: resolved.heading_level,
2967 style_id: default_paragraph_style_id.clone(),
2968 style_name: Some(resolved.name.clone()),
2969 numbering: resolved.numbering.clone().or_else(|| {
2970 default_paragraph_style
2971 .as_ref()
2972 .and_then(|value| value.numbering.clone())
2973 }),
2974 spacing: merge_paragraph_spacing(
2975 default_paragraph_style
2976 .as_ref()
2977 .and_then(|value| value.spacing.as_ref()),
2978 resolved.spacing.as_ref(),
2979 ),
2980 indent: merge_paragraph_indent(
2981 default_paragraph_style
2982 .as_ref()
2983 .and_then(|value| value.indent.as_ref()),
2984 resolved.indent.as_ref(),
2985 ),
2986 background_color: merge_paragraph_background_color(
2987 default_paragraph_style
2988 .as_ref()
2989 .and_then(|value| value.background_color.as_deref()),
2990 resolved.background_color.as_deref(),
2991 ),
2992 borders: merge_paragraph_border_sets(
2993 default_paragraph_style
2994 .as_ref()
2995 .and_then(|value| value.borders.as_ref()),
2996 resolved.borders.as_ref(),
2997 ),
2998 tab_stops: merge_paragraph_tab_stops(
2999 default_paragraph_style
3000 .as_ref()
3001 .and_then(|value| value.tab_stops.as_deref()),
3002 resolved.tab_stops.as_deref(),
3003 ),
3004 contextual_spacing: merge_paragraph_boolean(
3005 default_paragraph_style
3006 .as_ref()
3007 .and_then(|value| value.contextual_spacing),
3008 resolved.contextual_spacing,
3009 ),
3010 keep_next: merge_paragraph_boolean(
3011 default_paragraph_style
3012 .as_ref()
3013 .and_then(|value| value.keep_next),
3014 resolved.keep_next,
3015 ),
3016 keep_lines: merge_paragraph_boolean(
3017 default_paragraph_style
3018 .as_ref()
3019 .and_then(|value| value.keep_lines),
3020 resolved.keep_lines,
3021 ),
3022 widow_control: merge_paragraph_boolean(
3023 default_paragraph_style
3024 .as_ref()
3025 .and_then(|value| value.widow_control),
3026 resolved.widow_control,
3027 ),
3028 page_break_before: merge_paragraph_boolean(
3029 default_paragraph_style
3030 .as_ref()
3031 .and_then(|value| value.page_break_before),
3032 resolved.page_break_before,
3033 ),
3034 drop_cap: None,
3035 });
3036
3037 let mut table_paragraph_spacing_by_style_id = HashMap::new();
3038 for (style_id, _) in raw_styles_by_id
3039 .iter()
3040 .filter(|(_, style)| style.style_type == RawStyleType::Table)
3041 {
3042 let mut chain = Vec::new();
3043 let mut cursor = Some(style_id.clone());
3044 let mut seen = HashSet::new();
3045 while let Some(id) = cursor {
3046 if !seen.insert(id.clone()) {
3047 break;
3048 }
3049 let Some(chain_style) = raw_styles_by_id.get(&id) else {
3050 break;
3051 };
3052 if chain_style.style_type != RawStyleType::Table {
3053 break;
3054 }
3055 chain.push(chain_style);
3056 cursor = chain_style.based_on_id.clone();
3057 }
3058 let mut merged_spacing: Option<crate::model::ParagraphSpacing> = None;
3059 for chain_style in chain.iter().rev() {
3060 merged_spacing = crate::parse::util::merge_paragraph_spacing(
3061 merged_spacing.as_ref(),
3062 chain_style.spacing.as_ref(),
3063 );
3064 }
3065 if let Some(spacing) = merged_spacing {
3066 table_paragraph_spacing_by_style_id.insert(style_id.clone(), spacing);
3067 }
3068 }
3069
3070 ParsedStyleSheet {
3071 paragraph_styles,
3072 paragraph_style_by_id,
3073 run_style_by_id,
3074 table_style_by_id,
3075 default_table_style_id,
3076 table_paragraph_spacing_by_style_id,
3077 doc_default_paragraph_spacing: default_paragraph_style
3078 .as_ref()
3079 .and_then(|style| style.spacing.clone()),
3080 default_paragraph_text_alignment: default_paragraph_style
3081 .as_ref()
3082 .and_then(|style| style.text_alignment),
3083 default_paragraph_style: merged_default_paragraph_style,
3084 default_paragraph_style_id,
3085 default_run_style: merge_text_styles(
3086 default_run_style,
3087 resolved_default_paragraph_style.and_then(|value| value.run_style),
3088 ),
3089 theme_colors,
3090 theme_fonts,
3091 }
3092}
3093
3094pub fn clone_paragraph_style_definition(
3095 style: &ParagraphStyleDefinition,
3096) -> ParagraphStyleDefinition {
3097 style.clone()
3098}
3099
3100pub fn clone_numbering_definitions(definitions: &NumberingDefinitionSet) -> NumberingDefinitionSet {
3101 definitions.clone()
3102}
3103
3104#[cfg(test)]
3105mod table_box_spacing_tests {
3106 use super::parse_table_box_spacing;
3107
3108 #[test]
3109 fn parses_start_end_aliases() {
3110 let xml =
3111 r#"<w:tcMar><w:start w:w="0" w:type="dxa"/><w:end w:w="0" w:type="dxa"/></w:tcMar>"#;
3112 let spacing = parse_table_box_spacing(xml).expect("spacing");
3113 assert_eq!(spacing.left_twips, Some(0));
3114 assert_eq!(spacing.right_twips, Some(0));
3115 assert_eq!(spacing.top_twips, None);
3116 assert_eq!(spacing.bottom_twips, None);
3117 }
3118}
3119
3120#[cfg(all(test, not(target_arch = "wasm32")))]
3121mod document_style_cache_tests {
3122 #[test]
3123 fn shared_definitions_track_references_theme_changes_and_eviction() {
3124 for selector in (0..12).chain((0..12).rev()) {
3125 let selected = if selector % 2 == 0 { "a" } else { "b" };
3126 let font = if selector % 3 == 0 { "Serif" } else { "Sans" };
3127 let bold = if selector % 4 == 0 { "0" } else { "1" };
3128 let mut package = crate::package::OoxmlPackage {
3129 parts: Default::default(),
3130 binary_assets: Default::default(),
3131 warnings: vec![],
3132 };
3133 for (name, content) in [
3134 ("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>")),
3135 ("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>")),
3136 ("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>")),
3137 ] { package.parts.insert(name.into(), crate::package::OoxmlPart { name: name.into(), content }); }
3138 let expected = super::parse_document_style_sheet(&package);
3139 let actual = super::acquire_document_style_sheet(&package);
3140 assert_eq!(&*actual, &expected);
3141 let (mut metadata, _) = super::release_document_style_metadata(actual);
3142 if let Some(style) = metadata.first_mut() {
3143 style.name = "Changed".into();
3144 }
3145 assert_eq!(&*super::acquire_document_style_sheet(&package), &expected);
3146 }
3147 }
3148}
3149
3150
3151#[cfg(all(test, not(target_arch = "wasm32")))]
3152mod bounded_large_definition_tests {
3153 use super::*;
3154
3155 #[test]
3156 fn large_definitions_preserve_values_and_resident_budget() {
3157 for value in ["first", "second", "third", "first"] {
3158 let mut pkg = crate::package::OoxmlPackage {
3159 parts: Default::default(), binary_assets: Default::default(), warnings: vec![],
3160 };
3161 let name = format!("{value}{}", "x".repeat(140 * 1024));
3162 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>");
3163 pkg.parts.insert("word/styles.xml".into(), crate::package::OoxmlPart { name: "word/styles.xml".into(), content });
3164 let expected = parse_document_style_sheet(&pkg);
3165 for _ in 0..2 {
3166 assert_eq!(*acquire_document_style_sheet(&pkg), expected);
3167 DOCUMENT_STYLE_CACHE.read(|cache| {
3168 assert!(cache.len() <= 4);
3169 assert!(cache.iter().map(|entry| entry.source_bytes).sum::<usize>() <= 512 * 1024);
3170 });
3171 }
3172 }
3173 }
3174}