1use crate::model::{
2 ParagraphDropCap, ParagraphDropCapType, ParagraphSpacing, ParagraphStyle,
3 ParagraphTextAlignment, SharedParagraphStyle, SharedTextStyle, TextLigatures, TextStyle, VerticalAlign,
4};
5use crate::parse::context::{ParseContext, ThemeFontMap};
6use crate::parse::re;
7use crate::parse::util::{
8 active_xml as prefer_alternate_content_choice, merge_paragraph_boolean,
9 merge_paragraph_border_sets, merge_paragraph_indent, merge_paragraph_spacing,
10 merge_paragraph_tab_stops, normalize_heading_level, normalize_hex_color,
11 parse_paragraph_border_set_from_xml, parse_paragraph_indent_from_xml,
12 parse_paragraph_numbering_from_xml, parse_paragraph_numbering_parts_from_xml,
13 parse_paragraph_shading_from_xml, parse_paragraph_spacing_from_xml,
14 parse_paragraph_tab_stops_from_xml, parse_text_run_border_style, resolve_theme_font,
15 to_model_alignment,
16};
17use crate::xml::{
18 extract_balanced_tag_blocks, get_attribute, parse_integer_attribute, parse_on_off_attribute,
19 parse_underline_attribute,
20};
21
22pub fn parse_paragraph_drop_cap_from_xml(
23 paragraph_properties_xml: &str,
24) -> Option<ParagraphDropCap> {
25 let frame_pr_tag =
26 super::scan::find_tag_token_ref(paragraph_properties_xml, "w:framePr").unwrap_or_default();
27 if frame_pr_tag.is_empty() {
28 return None;
29 }
30 let drop_cap_raw =
31 get_attribute(&frame_pr_tag, "w:dropCap").map(|v| v.trim().to_ascii_lowercase())?;
32 let drop_cap_type = match drop_cap_raw.as_str() {
33 "drop" => ParagraphDropCapType::Drop,
34 "margin" => ParagraphDropCapType::Margin,
35 _ => return None,
36 };
37 let lines = parse_integer_attribute(&frame_pr_tag, "w:lines");
38 let wrap = get_attribute(&frame_pr_tag, "w:wrap").map(|v| v.trim().to_string());
39 let horizontal_anchor = get_attribute(&frame_pr_tag, "w:hAnchor").map(|v| v.trim().to_string());
40 let vertical_anchor = get_attribute(&frame_pr_tag, "w:vAnchor").map(|v| v.trim().to_string());
41 let x_twips = parse_integer_attribute(&frame_pr_tag, "w:x");
42 let y_twips = parse_integer_attribute(&frame_pr_tag, "w:y");
43 let horizontal_space_twips = parse_integer_attribute(&frame_pr_tag, "w:hSpace");
44 let vertical_space_twips = parse_integer_attribute(&frame_pr_tag, "w:vSpace");
45 Some(ParagraphDropCap {
46 drop_cap_type,
47 lines: lines.filter(|l| *l > 0),
48 wrap,
49 horizontal_anchor,
50 vertical_anchor,
51 x_twips,
52 y_twips,
53 horizontal_space_twips,
54 vertical_space_twips,
55 })
56}
57
58pub(crate) fn parse_paragraph_text_alignment_from_xml(xml: &str) -> Option<ParagraphTextAlignment> {
59 let tag = super::styles::direct_table_property(xml, "w:textAlignment")?;
60 match get_attribute(tag, "w:val")?.to_ascii_lowercase().as_str() {
61 "auto" => Some(ParagraphTextAlignment::Auto),
62 "top" => Some(ParagraphTextAlignment::Top),
63 "center" => Some(ParagraphTextAlignment::Center),
64 "baseline" => Some(ParagraphTextAlignment::Baseline),
65 "bottom" => Some(ParagraphTextAlignment::Bottom),
66 _ => None,
67 }
68}
69
70pub fn parse_paragraph_align_from_xml(xml: &str) -> Option<crate::model::ParagraphAlignment> {
71 let alignment = super::scan::find_attribute_value_in_tag(xml, "w:jc", "w:val")
72 .filter(|value| !value.is_empty());
73 to_model_alignment(alignment.as_deref())
74}
75
76pub(crate) fn parse_text_ligatures_from_xml(xml: &str) -> Option<TextLigatures> {
77 super::scan::find_attribute_value_in_tag(xml, "w14:ligatures", "w14:val")
78 .as_deref()
79 .and_then(TextLigatures::from_xml_value)
80}
81
82pub(crate) fn parse_contextual_alternates_from_xml(xml: &str) -> Option<bool> {
83 let tag = super::scan::find_tag_token(xml, "w14:cntxtAlts")?;
84 match get_attribute(&tag, "w14:val").as_deref() {
85 None | Some("true" | "1") => Some(true),
86 Some("false" | "0") => Some(false),
87 _ => None,
88 }
89}
90
91pub(crate) fn parse_character_scale_percent_value(raw: &str) -> Option<u16> {
92 let value = raw.trim();
93 let (digits, negative) = if let Some(digits) = value.strip_suffix('%') {
94 (digits, false)
95 } else if let Some(digits) = value.strip_prefix('-') {
96 (digits, true)
97 } else {
98 (value.strip_prefix('+').unwrap_or(value), false)
99 };
100 if digits.is_empty() || !digits.bytes().all(|digit| digit.is_ascii_digit()) {
101 return None;
102 }
103 let significant_digits = digits.trim_start_matches('0');
104 let percent = if significant_digits.is_empty() {
105 0
106 } else {
107 significant_digits.parse::<u16>().ok()?
108 };
109 (percent <= 600 && (!negative || percent == 0)).then_some(percent)
110}
111
112pub(crate) fn parse_character_scale_percent_from_xml(xml: &str) -> Option<u16> {
113 let tag = super::scan::find_tag_token(xml, "w:w")?;
114 get_attribute(&tag, "w:val")
115 .as_deref()
116 .map(parse_character_scale_percent_value)
117 .unwrap_or(Some(100))
118}
119
120fn text_script_flags(xml: &str) -> u8 {
121 if xml.is_ascii() && !xml.contains("&#") {
122 return 0;
123 }
124 let mut flags = 0;
125 if let Some(pattern) = re::get(r"(?is)<(?:w|a):t\b[^>]*>([\s\S]*?)</(?:w|a):t>") {
126 for caps in pattern.captures_iter(xml) {
127 if let Some(text) = caps.get(1) {
128 for ch in crate::xml::decode_xml_entities(text.as_str()).chars() {
129 let code = ch as u32;
130 if matches!(code, 0x2e80..=0x9fff | 0xac00..=0xd7af) {
131 flags |= 1;
132 }
133 if matches!(code, 0x0590..=0x08ff | 0xfb1d..=0xfefc) {
134 flags |= 2;
135 }
136 }
137 }
138 }
139 }
140 flags
141}
142
143pub fn parse_text_style_from_xml(xml: &str, theme_fonts: &ThemeFontMap) -> Option<TextStyle> {
144 cached_text_style(xml, theme_fonts, text_script_flags(xml))
145}
146
147fn has_drawing_text_style_tags(xml: &str) -> bool {
148 xml.split('<').skip(1).any(|tag| {
149 if !super::scan::starts_with_ignore_ascii_case(tag, "a:") {
150 return false;
151 }
152 let tag = &tag[2..];
153 match tag.as_bytes().first().map(u8::to_ascii_lowercase) {
154 Some(b'r') => super::scan::starts_with_ignore_ascii_case(tag, "rPr"),
155 Some(b'd') => super::scan::starts_with_ignore_ascii_case(tag, "defRPr"),
156 Some(b'l') => super::scan::starts_with_ignore_ascii_case(tag, "latin"),
157 _ => false,
158 }
159 })
160}
161
162fn parse_text_style_with_scripts(
163 xml: &str,
164 theme_fonts: &ThemeFontMap,
165 scripts: u8,
166) -> Option<TextStyle> {
167 if xml.is_empty() {
168 return None;
169 }
170 let bold = parse_on_off_attribute(xml, "b");
171 let italic = parse_on_off_attribute(xml, "i");
172 let underline = parse_underline_attribute(xml);
173 let strike = parse_on_off_attribute(xml, "strike");
174 let all_caps = parse_on_off_attribute(xml, "caps");
175 let small_caps = parse_on_off_attribute(xml, "smallCaps");
176
177 let color_match = super::scan::find_attribute_value_in_tag(xml, "w:color", "w:val")
178 .filter(|value| !value.is_empty());
179 let highlight_match = super::scan::find_attribute_value_in_tag(xml, "w:highlight", "w:val")
180 .filter(|value| !value.is_empty());
181 let shading_tag = super::scan::find_tag_token_ref(xml, "w:shd");
182 let character_spacing_match =
183 super::scan::find_attribute_value_in_tag(xml, "w:spacing", "w:val");
184 let size_match = super::scan::find_attribute_value_in_tag(xml, "w:sz", "w:val")
185 .filter(|value| !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()));
186 let complex_size_match = super::scan::find_attribute_value_in_tag(xml, "w:szCs", "w:val")
187 .filter(|value| !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()));
188 let run_fonts_tag = super::scan::find_tag_token_ref(xml, "w:rFonts").unwrap_or_default();
189 let ascii_font = get_attribute(&run_fonts_tag, "w:ascii");
190 let h_ansi_font = get_attribute(&run_fonts_tag, "w:hAnsi");
191 let east_asia_font = get_attribute(&run_fonts_tag, "w:eastAsia");
192 let complex_script_font = get_attribute(&run_fonts_tag, "w:cs");
193 let ascii_theme_font = get_attribute(&run_fonts_tag, "w:asciiTheme");
194 let h_ansi_theme_font = get_attribute(&run_fonts_tag, "w:hAnsiTheme");
195 let east_asia_theme_font = get_attribute(&run_fonts_tag, "w:eastAsiaTheme");
196 let complex_script_theme_font = get_attribute(&run_fonts_tag, "w:csTheme");
197 let font_hint = get_attribute(&run_fonts_tag, "w:hint");
198 let language_tag = super::scan::find_tag_token_ref(xml, "w:lang").unwrap_or_default();
199 let language = get_attribute(&language_tag, "w:val");
200 let language_east_asia = get_attribute(&language_tag, "w:eastAsia");
201 let language_bidi = get_attribute(&language_tag, "w:bidi");
202 let right_to_left = parse_on_off_attribute(xml, "rtl");
203 let complex_script = parse_on_off_attribute(xml, "cs");
204 let vertical_align_match =
205 super::scan::find_attribute_value_in_tag(xml, "w:vertAlign", "w:val")
206 .filter(|value| !value.is_empty());
207 let drawing_xml = if has_drawing_text_style_tags(xml) {
208 xml
209 } else {
210 ""
211 };
212 let drawing_bold_match = regex_capture(drawing_xml, r#"(?i)<a:rPr\b[^>]*\bb="([^"]+)""#);
213 let drawing_italic_match = regex_capture(drawing_xml, r#"(?i)<a:rPr\b[^>]*\bi="([^"]+)""#);
214 let drawing_underline_match = regex_capture(drawing_xml, r#"(?i)<a:rPr\b[^>]*\bu="([^"]+)""#);
215 let drawing_strike_match =
216 regex_capture(drawing_xml, r#"(?i)<a:rPr\b[^>]*\b(?:strike|s)="([^"]+)""#);
217 let drawing_color_match = regex_capture(
218 drawing_xml,
219 r#"(?is)<a:rPr\b[\s\S]*?<a:(?:solidFill|srgbClr)\b[\s\S]*?<a:srgbClr\b[^>]*val="([^"]+)""#,
220 );
221 let drawing_size_match = regex_capture(drawing_xml, r#"(?i)<a:rPr\b[^>]*\bsz="(\d+)""#);
222 let drawing_font_match = regex_capture(
223 drawing_xml,
224 r#"(?is)<a:rPr\b[\s\S]*?<a:latin\b[^>]*typeface="([^"]+)""#,
225 );
226 let drawing_default_font_match = regex_capture(
227 drawing_xml,
228 r#"(?is)<a:defRPr\b[\s\S]*?<a:latin\b[^>]*typeface="([^"]+)""#,
229 );
230 let drawing_any_latin_match =
231 regex_capture(drawing_xml, r#"(?i)<a:latin\b[^>]*typeface="([^"]+)""#);
232 let run_border_tag = super::scan::find_tag_token_ref(xml, "w:bdr");
233
234 let contains_east_asia_glyphs = scripts & 1 != 0;
235 let contains_complex_script_glyphs = scripts & 2 != 0;
236
237 let mut style = TextStyle {
238 bold: None,
239 italic: None,
240 underline: None,
241 strike: None,
242 all_caps: None,
243 small_caps: None,
244 color: None,
245 highlight: None,
246 background_color: None,
247 font_size_pt: None,
248 font_size_cs_pt: None,
249 font_family: None,
250 source_font_family: None,
251 font_family_ascii: None,
252 font_family_h_ansi: None,
253 font_family_east_asia: None,
254 font_family_cs: None,
255 font_theme_ascii: None,
256 font_theme_h_ansi: None,
257 font_theme_east_asia: None,
258 font_theme_cs: None,
259 resolved_font_family_ascii: None,
260 resolved_font_family_h_ansi: None,
261 resolved_font_family_east_asia: None,
262 resolved_font_family_cs: None,
263 font_hint: None,
264 language: None,
265 language_east_asia: None,
266 language_bidi: None,
267 right_to_left: None,
268 complex_script: None,
269 character_spacing_twips: None,
270 character_scale_percent: None,
271 source_character_scale: None,
272 kerning_min_font_size_pt: None,
273 ligatures: None,
274 contextual_alternates: None,
275 vertical_align: None,
276 run_border: None,
277 text_shadow: None,
278 text_outline: None,
279 };
280 let mut has_any = false;
281
282 if let Some(v) = bold {
283 style.bold = Some(v);
284 has_any = true;
285 }
286 if let Some(v) = italic {
287 style.italic = Some(v);
288 has_any = true;
289 }
290 if let Some(v) = underline {
291 style.underline = Some(v);
292 has_any = true;
293 }
294 if let Some(v) = strike {
295 style.strike = Some(v);
296 has_any = true;
297 }
298 if let Some(v) = all_caps {
299 style.all_caps = Some(v);
300 has_any = true;
301 }
302 if let Some(v) = small_caps {
303 style.small_caps = Some(v);
304 has_any = true;
305 }
306
307 if style.bold.is_none() {
308 if let Some(value) = drawing_bold_match {
309 let value = value.to_ascii_lowercase();
310 style.bold = Some(value != "0" && value != "false");
311 has_any = true;
312 }
313 }
314 if style.italic.is_none() {
315 if let Some(value) = drawing_italic_match {
316 let value = value.to_ascii_lowercase();
317 style.italic = Some(value != "0" && value != "false");
318 has_any = true;
319 }
320 }
321 if style.underline.is_none() {
322 if let Some(value) = drawing_underline_match {
323 let value = value.to_ascii_lowercase();
324 style.underline = Some(value != "none" && value != "false" && value != "0");
325 has_any = true;
326 }
327 }
328 if style.strike.is_none() {
329 if let Some(value) = drawing_strike_match {
330 let value = value.to_ascii_lowercase();
331 style.strike =
332 Some(value != "nostrike" && value != "none" && value != "false" && value != "0");
333 has_any = true;
334 }
335 }
336
337 if let Some(color) = normalize_hex_color(color_match.as_deref()) {
338 style.color = Some(color);
339 has_any = true;
340 } else if let Some(color) = normalize_hex_color(drawing_color_match.as_deref()) {
341 style.color = Some(color);
342 has_any = true;
343 }
344
345 if let Some(highlight) = highlight_match {
346 style.highlight = Some(highlight);
347 has_any = true;
348 }
349
350 if let Some(tag) = shading_tag.as_deref() {
351 if let Some(fill) = normalize_hex_color(get_attribute(tag, "w:fill").as_deref()) {
352 style.background_color = Some(fill);
353 has_any = true;
354 }
355 }
356
357 style.character_scale_percent = parse_character_scale_percent_from_xml(xml);
358 style.source_character_scale = super::scan::find_attribute_value_in_tag(xml, "w:w", "w:val");
359 has_any |= style.character_scale_percent.is_some() || style.source_character_scale.is_some();
360
361 if let Some(spacing) = character_spacing_match.and_then(|v| v.parse::<i64>().ok()) {
362 style.character_spacing_twips = Some(spacing);
363 has_any = true;
364 }
365
366 if let Some(ligatures) = parse_text_ligatures_from_xml(xml) {
367 style.ligatures = Some(ligatures);
368 has_any = true;
369 }
370 if let Some(contextual_alternates) = parse_contextual_alternates_from_xml(xml) {
371 style.contextual_alternates = Some(contextual_alternates);
372 has_any = true;
373 }
374
375 if let Some(kerning) = super::scan::find_attribute_value_in_tag(xml, "w:kern", "w:val")
376 .filter(|value| !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
377 .and_then(|value| value.parse::<f64>().ok())
378 {
379 if kerning.is_finite() && kerning >= 0.0 {
380 style.kerning_min_font_size_pt = Some(kerning / 2.0);
381 has_any = true;
382 }
383 }
384
385 if let Some(size) = size_match.and_then(|v| v.parse::<f64>().ok()) {
386 style.font_size_pt = Some(size / 2.0);
387 has_any = true;
388 } else if let Some(size) = drawing_size_match.and_then(|v| v.parse::<f64>().ok()) {
389 style.font_size_pt = Some(size / 100.0);
390 has_any = true;
391 }
392
393 if let Some(size) = complex_size_match.and_then(|v| v.parse::<f64>().ok()) {
394 style.font_size_cs_pt = Some(size / 2.0);
395 has_any = true;
396 }
397
398 style.font_family_ascii = ascii_font.clone();
399 style.font_family_h_ansi = h_ansi_font.clone();
400 style.font_family_east_asia = east_asia_font.clone();
401 style.font_family_cs = complex_script_font.clone();
402 style.font_theme_ascii = ascii_theme_font.clone();
403 style.font_theme_h_ansi = h_ansi_theme_font.clone();
404 style.font_theme_east_asia = east_asia_theme_font.clone();
405 style.font_theme_cs = complex_script_theme_font.clone();
406 style.resolved_font_family_ascii = ascii_theme_font
407 .as_deref()
408 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
409 .or_else(|| ascii_font.clone());
410 style.resolved_font_family_h_ansi = h_ansi_theme_font
411 .as_deref()
412 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
413 .or_else(|| h_ansi_font.clone());
414 style.resolved_font_family_east_asia = east_asia_theme_font
415 .as_deref()
416 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
417 .or_else(|| east_asia_font.clone());
418 style.resolved_font_family_cs = complex_script_theme_font
419 .as_deref()
420 .and_then(|token| resolve_theme_font(Some(token), theme_fonts))
421 .or_else(|| complex_script_font.clone());
422 style.font_hint = font_hint;
423 style.language = language;
424 style.language_east_asia = language_east_asia;
425 style.language_bidi = language_bidi;
426 style.right_to_left = right_to_left;
427 style.complex_script = complex_script;
428 has_any |= style.font_family_ascii.is_some()
429 || style.font_family_h_ansi.is_some()
430 || style.font_family_east_asia.is_some()
431 || style.font_family_cs.is_some()
432 || style.font_theme_ascii.is_some()
433 || style.font_theme_h_ansi.is_some()
434 || style.font_theme_east_asia.is_some()
435 || style.font_theme_cs.is_some()
436 || style.resolved_font_family_ascii.is_some()
437 || style.resolved_font_family_h_ansi.is_some()
438 || style.resolved_font_family_east_asia.is_some()
439 || style.resolved_font_family_cs.is_some()
440 || style.font_hint.is_some()
441 || style.language.is_some()
442 || style.language_east_asia.is_some()
443 || style.language_bidi.is_some()
444 || style.right_to_left.is_some()
445 || style.complex_script.is_some();
446
447 let run_font_family = ascii_font.clone().or(h_ansi_font.clone());
448 let run_theme_font_token = ascii_theme_font.clone().or(h_ansi_theme_font.clone());
449 let east_asia_fallback_font = if contains_east_asia_glyphs {
450 east_asia_font.clone()
451 } else {
452 None
453 };
454 let east_asia_fallback_theme_token = if contains_east_asia_glyphs {
455 east_asia_theme_font.clone()
456 } else {
457 None
458 };
459 let complex_script_fallback_font = if contains_complex_script_glyphs {
460 complex_script_font.clone()
461 } else {
462 None
463 };
464 let complex_script_fallback_theme_token = if contains_complex_script_glyphs {
465 complex_script_theme_font.clone()
466 } else {
467 None
468 };
469 let symbol_fallback_font = {
470 let font = east_asia_font.clone().or(complex_script_font.clone());
471 font.as_deref()
472 .filter(|f| {
473 re::get_unchecked(r"(?i)(symbol|emoji|dingbats?|wingdings|webdings)").is_match(f)
474 })
475 .map(|_| east_asia_font.clone().or(complex_script_font.clone()))
476 .flatten()
477 };
478
479 if let Some(family) = run_font_family {
480 style.font_family = Some(family);
481 has_any = true;
482 } else if let Some(token) = run_theme_font_token.as_deref() {
483 if let Some(family) = resolve_theme_font(Some(token), theme_fonts) {
484 style.font_family = Some(family);
485 has_any = true;
486 }
487 } else if let Some(family) = east_asia_fallback_font {
488 style.font_family = Some(family);
489 has_any = true;
490 } else if let Some(token) = east_asia_fallback_theme_token.as_deref() {
491 if let Some(family) = resolve_theme_font(Some(token), theme_fonts) {
492 style.font_family = Some(family);
493 has_any = true;
494 }
495 } else if let Some(family) = complex_script_fallback_font {
496 style.font_family = Some(family);
497 has_any = true;
498 } else if let Some(token) = complex_script_fallback_theme_token.as_deref() {
499 if let Some(family) = resolve_theme_font(Some(token), theme_fonts) {
500 style.font_family = Some(family);
501 has_any = true;
502 }
503 } else if let Some(family) = symbol_fallback_font {
504 style.font_family = Some(family);
505 has_any = true;
506 } else if let Some(family) = drawing_font_match {
507 style.font_family = Some(family);
508 has_any = true;
509 } else if let Some(family) = drawing_default_font_match {
510 style.font_family = Some(family);
511 has_any = true;
512 } else if let Some(family) = drawing_any_latin_match {
513 style.font_family = Some(family);
514 has_any = true;
515 }
516 style.source_font_family = style.font_family.clone();
517
518 if let Some(value) = vertical_align_match.map(|v| v.to_ascii_lowercase()) {
519 style.vertical_align = match value.as_str() {
520 "superscript" => Some(VerticalAlign::Superscript),
521 "subscript" => Some(VerticalAlign::Subscript),
522 _ => None,
523 };
524 if style.vertical_align.is_some() {
525 has_any = true;
526 }
527 }
528
529 if let Some(border) = parse_text_run_border_style(run_border_tag.as_deref()) {
530 style.run_border = Some(border);
531 has_any = true;
532 }
533
534 has_any |= super::text_effects::parse_into(&mut style, xml);
535
536 if has_any {
537 Some(style)
538 } else {
539 None
540 }
541}
542
543fn resolve_toggle_property(
548 default_value: Option<bool>,
549 paragraph_value: Option<bool>,
550 character_value: Option<bool>,
551 direct_value: Option<bool>,
552) -> Option<bool> {
553 if direct_value.is_some() {
554 return direct_value;
555 }
556 if paragraph_value == Some(true) && character_value == Some(true) {
557 return Some(false);
558 }
559 character_value.or(paragraph_value).or(default_value)
560}
561
562type RawStyleEntries =
563 std::collections::HashMap<String, Vec<(u8, ThemeFontMap, Option<SharedTextStyle>)>>;
564
565#[derive(Default)]
566struct RunStyleCache {
567 entries: RawStyleEntries,
568 resolved: std::collections::HashMap<String, Vec<ResolvedRunStyle>>,
569 paragraphs:
570 std::collections::HashMap<String, Vec<(Option<ParagraphSpacing>, Option<SharedParagraphStyle>)>>,
571 mark_default: Option<CachedMarkDefaultStyle>,
572 #[cfg(not(target_arch = "wasm32"))]
573 sheet: Option<std::sync::Arc<super::context::ParsedStyleSheet>>,
574}
575
576struct CachedMarkDefaultStyle {
577 document: Option<TextStyle>,
578 bound_document: bool,
579 table: Option<TextStyle>,
580 style: Option<SharedTextStyle>,
581}
582
583struct ResolvedRunStyle {
584 scripts: u8,
585 paragraph: Option<String>,
586 table: Option<Box<TextStyle>>,
587 style: Option<SharedTextStyle>,
588}
589
590thread_local! {
591 static RUN_STYLE_CACHE: std::cell::RefCell<Option<RunStyleCache>> = const { std::cell::RefCell::new(None) };
592}
593
594pub(crate) struct RunStyleCacheScope(Option<RunStyleCache>);
595
596#[cfg(not(target_arch = "wasm32"))]
597thread_local! {
598 static RETAINED_RAW_STYLES: std::cell::RefCell<Option<RawStyleEntries>> = const { std::cell::RefCell::new(None) };
599 static RETAINED_RESOLVED_STYLES: std::cell::RefCell<Option<RunStyleCache>> = const { std::cell::RefCell::new(None) };
600}
601
602pub(crate) fn bind_style_cache(sheet: &super::context::ParsedStyleSheet) {
603 #[cfg(not(target_arch = "wasm32"))]
604 if let Some(identity) = super::styles::cached_document_style_identity(sheet) {
605 bind_shared_style_cache(identity);
606 }
607 #[cfg(target_arch = "wasm32")]
608 let _ = sheet;
609}
610
611#[cfg(not(target_arch = "wasm32"))]
612fn bind_shared_style_cache(sheet: std::sync::Arc<super::context::ParsedStyleSheet>) {
613 RUN_STYLE_CACHE.with(|current| {
614 let mut current = current.borrow_mut();
615 let Some(current) = current.as_mut() else { return; };
616 if current.sheet.as_ref().is_some_and(|previous| std::sync::Arc::ptr_eq(previous, &sheet)) {
617 return;
618 }
619 let previous = RETAINED_RESOLVED_STYLES.with(|retained| retained.take())
620 .filter(|previous| previous.sheet.as_ref().is_some_and(|previous| std::sync::Arc::ptr_eq(previous, &sheet)));
621 current.resolved.clear();
622 current.paragraphs.clear();
623 current.mark_default = None;
624 current.sheet = Some(sheet);
625 if let Some(previous) = previous {
626 current.resolved = previous.resolved;
627 current.paragraphs = previous.paragraphs;
628 current.mark_default = previous.mark_default;
629 }
630 });
631}
632
633pub(crate) fn run_style_cache_scope() -> RunStyleCacheScope {
634 #[allow(unused_mut)]
635 let mut current = RunStyleCache::default();
636 #[cfg(not(target_arch = "wasm32"))]
637 if RUN_STYLE_CACHE.with(|cache| cache.borrow().is_none()) {
638 current.entries = RETAINED_RAW_STYLES
639 .with(|cache| cache.take())
640 .unwrap_or_default();
641 }
642 RunStyleCacheScope(RUN_STYLE_CACHE.with(|cache| cache.replace(Some(current))))
643}
644
645impl Drop for RunStyleCacheScope {
646 fn drop(&mut self) {
647 #[cfg(not(target_arch = "wasm32"))]
648 let outer = self.0.is_none();
649 let _current = RUN_STYLE_CACHE.with(|cache| cache.replace(self.0.take()));
650 #[cfg(not(target_arch = "wasm32"))]
651 if outer {
652 if let Some(mut current) = _current {
653 if current.entries.len() <= 512
654 && current.entries.keys().map(String::len).sum::<usize>() <= 512 * 1024
655 && current.entries.values().map(Vec::len).sum::<usize>() <= 1024
656 {
657 RETAINED_RAW_STYLES
658 .with(|cache| cache.replace(Some(std::mem::take(&mut current.entries))));
659 }
660 current.entries = RawStyleEntries::default();
661 if current.sheet.is_some()
662 && current.resolved.len() + current.paragraphs.len() <= 512
663 && current.resolved.keys().chain(current.paragraphs.keys()).map(String::len).sum::<usize>() <= 512 * 1024
664 && current.resolved.values().map(Vec::len).sum::<usize>()
665 + current.paragraphs.values().map(Vec::len).sum::<usize>() <= 1024
666 {
667 RETAINED_RESOLVED_STYLES.with(|cache| cache.replace(Some(current)));
668 }
669 }
670 }
671 }
672}
673
674fn same_mark_default_style(left: Option<&TextStyle>, right: Option<&TextStyle>) -> bool {
675 if left != right {
676 return false;
677 }
678 let (Some(left), Some(right)) = (left, right) else {
679 return true;
680 };
681 left.font_size_pt.map(f64::to_bits) == right.font_size_pt.map(f64::to_bits)
682 && left.font_size_cs_pt.map(f64::to_bits) == right.font_size_cs_pt.map(f64::to_bits)
683 && left.kerning_min_font_size_pt.map(f64::to_bits)
684 == right.kerning_min_font_size_pt.map(f64::to_bits)
685 && left
686 .run_border
687 .as_ref()
688 .and_then(|border| border.size_eighth_pt)
689 .map(f64::to_bits)
690 == right
691 .run_border
692 .as_ref()
693 .and_then(|border| border.size_eighth_pt)
694 .map(f64::to_bits)
695 && left
696 .run_border
697 .as_ref()
698 .and_then(|border| border.space_pt)
699 .map(f64::to_bits)
700 == right
701 .run_border
702 .as_ref()
703 .and_then(|border| border.space_pt)
704 .map(f64::to_bits)
705}
706
707fn mark_document_is_bound(cache: &RunStyleCache, document: Option<&TextStyle>) -> bool {
708 #[cfg(not(target_arch = "wasm32"))]
709 {
710 cache.sheet.as_ref().is_some_and(|sheet| {
711 match (sheet.default_run_style.as_ref(), document) {
712 (Some(left), Some(right)) => std::ptr::eq(left, right),
713 (None, None) => true,
714 _ => false,
715 }
716 })
717 }
718 #[cfg(target_arch = "wasm32")]
719 {
720 let _ = (cache, document);
721 false
722 }
723}
724
725pub(crate) fn cached_mark_default_style(
726 document: Option<&TextStyle>,
727 table: Option<&TextStyle>,
728) -> Option<SharedTextStyle> {
729 let cached = RUN_STYLE_CACHE.with(|cache| {
730 let cache = cache.borrow();
731 let cache = cache.as_ref()?;
732 let entry = cache.mark_default.as_ref()?;
733 ((entry.bound_document && mark_document_is_bound(cache, document)
734 || same_mark_default_style(entry.document.as_ref(), document))
735 && same_mark_default_style(entry.table.as_ref(), table))
736 .then(|| entry.style.clone())
737 });
738 if let Some(style) = cached {
739 return style;
740 }
741 let style: Option<SharedTextStyle> =
742 super::util::merge_text_style_refs(&[document, table]).map(Into::into);
743 RUN_STYLE_CACHE.with(|cache| {
744 if let Some(cache) = cache.borrow_mut().as_mut() {
745 cache.mark_default = Some(CachedMarkDefaultStyle {
746 document: document.cloned(),
747 bound_document: mark_document_is_bound(cache, document),
748 table: table.cloned(),
749 style: style.clone(),
750 });
751 }
752 });
753 style
754}
755
756pub(crate) fn cached_definition_style(xml: &str, fonts: &ThemeFontMap) -> Option<TextStyle> {
757 cached_text_style(xml, fonts, 128)
758}
759
760fn cached_text_style(xml: &str, fonts: &ThemeFontMap, scripts: u8) -> Option<TextStyle> {
761 if xml.is_empty() {
762 return None;
763 }
764 if RUN_STYLE_CACHE.with(|cache| cache.borrow().is_none()) {
765 return if scripts == 128 {
766 super::styles::parse_definition_style_uncached(xml, fonts)
767 } else {
768 parse_text_style_with_scripts(xml, fonts, scripts)
769 };
770 }
771 cached_shared_text_style(xml, fonts, scripts).map(SharedTextStyle::into_owned)
772}
773
774pub(crate) fn parse_shared_text_style_from_xml(
775 xml: &str,
776 fonts: &ThemeFontMap,
777) -> Option<SharedTextStyle> {
778 cached_shared_text_style(xml, fonts, text_script_flags(xml))
779}
780
781fn cached_shared_text_style(
782 xml: &str,
783 fonts: &ThemeFontMap,
784 scripts: u8,
785) -> Option<SharedTextStyle> {
786 if xml.is_empty() {
787 return None;
788 }
789 let cached = RUN_STYLE_CACHE.with(|cache| {
790 let cache = cache.borrow();
791 cache
792 .as_ref()?
793 .entries
794 .get(xml)?
795 .iter()
796 .find(|(flags, theme, _)| *flags == scripts && theme == fonts)
797 .map(|(_, _, style)| style.clone())
798 });
799 if let Some(style) = cached {
800 return style;
801 }
802 let style = if scripts == 128 {
803 super::styles::parse_definition_style_uncached(xml, fonts)
804 } else {
805 parse_text_style_with_scripts(xml, fonts, scripts)
806 }
807 .map(SharedTextStyle::from);
808 if xml.len() <= 8192 {
809 RUN_STYLE_CACHE.with(|cache| {
810 if let Some(cache) = cache
811 .borrow_mut()
812 .as_mut()
813 .filter(|cache| cache.entries.len() < 4096)
814 {
815 let variants = cache
816 .entries
817 .entry(xml.to_owned())
818 .or_insert_with(|| Vec::with_capacity(1));
819 if variants.len() < 16 {
820 variants.push((scripts, fonts.clone(), style.clone()));
821 }
822 }
823 });
824 }
825 return style;
826}
827
828fn ordinary_run_properties(run_xml: &str) -> Option<&str> {
829 let mut properties = None;
830 let mut seen = false;
831 if let Some(ordinary) = crate::xml_index::for_each_child(run_xml, |name, xml| {
832 seen = true;
833 if name.eq_ignore_ascii_case("w:rPr") {
834 return properties.replace(xml).is_none();
835 }
836 if name.eq_ignore_ascii_case("w:fldChar") && !xml.ends_with("/>") {
837 return false;
838 }
839 [
840 "w:t",
841 "w:tab",
842 "w:br",
843 "w:cr",
844 "w:sym",
845 "w:noBreakHyphen",
846 "w:softHyphen",
847 "w:footnoteReference",
848 "w:endnoteReference",
849 "w:instrText",
850 "w:fldChar",
851 "w:lastRenderedPageBreak",
852 ]
853 .iter()
854 .any(|tag| name.eq_ignore_ascii_case(tag))
855 }) {
856 return (ordinary && seen).then(|| properties.unwrap_or(""));
857 }
858 let ranges = crate::xml::extract_direct_child_tag_ranges(run_xml);
859 let ordinary = !ranges.is_empty()
860 && ranges
861 .iter()
862 .filter(|range| range.tag_name.eq_ignore_ascii_case("w:rPr"))
863 .count()
864 <= 1
865 && ranges.iter().all(|range| {
866 if range.tag_name.eq_ignore_ascii_case("w:fldChar")
867 && !run_xml[range.start..range.end].ends_with("/>")
868 {
869 return false;
870 }
871 [
872 "w:rPr",
873 "w:t",
874 "w:tab",
875 "w:br",
876 "w:cr",
877 "w:sym",
878 "w:noBreakHyphen",
879 "w:softHyphen",
880 "w:footnoteReference",
881 "w:endnoteReference",
882 "w:instrText",
883 "w:fldChar",
884 "w:lastRenderedPageBreak",
885 ]
886 .iter()
887 .any(|tag| range.tag_name.eq_ignore_ascii_case(tag))
888 });
889 if ordinary {
890 let properties = ranges
891 .iter()
892 .find(|range| range.tag_name.eq_ignore_ascii_case("w:rPr"))
893 .map(|range| &run_xml[range.start..range.end])
894 .unwrap_or("");
895 return Some(properties);
896 }
897 None
898}
899
900fn parse_direct_run_style(
901 run_xml: &str,
902 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
903) -> Option<TextStyle> {
904 let text_box_content = prefer_alternate_content_choice(run_xml);
905 let text_box_run_xml = extract_balanced_tag_blocks(&text_box_content, "w:txbxContent")
906 .into_iter()
907 .next()
908 .and_then(|txbx| extract_balanced_tag_blocks(&txbx, "w:r").into_iter().next());
909 parse_text_style_from_xml(
910 text_box_run_xml.as_deref().unwrap_or(run_xml),
911 &context.styles().theme_fonts,
912 )
913}
914
915pub fn parse_run_style(
916 run_xml: &str,
917 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
918 paragraph_style_id: Option<&str>,
919) -> Option<TextStyle> {
920 parse_run_style_in_table(run_xml, context, paragraph_style_id, None)
921 .map(SharedTextStyle::into_owned)
922}
923
924pub(crate) fn parse_run_style_in_table(
925 run_xml: &str,
926 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
927 paragraph_style_id: Option<&str>,
928 table_run_style: Option<&TextStyle>,
929) -> Option<SharedTextStyle> {
930 let properties = ordinary_run_properties(run_xml);
931 resolve_run_style(
932 run_xml,
933 properties,
934 context,
935 paragraph_style_id,
936 table_run_style,
937 )
938}
939
940pub(crate) fn parse_mark_run_style(
941 properties: &str,
942 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
943 paragraph_style_id: Option<&str>,
944 table_run_style: Option<&TextStyle>,
945) -> Option<SharedTextStyle> {
946 resolve_run_style(
947 properties,
948 Some(properties),
949 context,
950 paragraph_style_id,
951 table_run_style,
952 )
953}
954
955fn resolve_run_style(
956 run_xml: &str,
957 properties: Option<&str>,
958 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
959 paragraph_style_id: Option<&str>,
960 table_run_style: Option<&TextStyle>,
961) -> Option<SharedTextStyle> {
962 let scripts = if properties.is_some_and(|xml| !xml.is_empty()) {
963 text_script_flags(run_xml)
964 } else {
965 0
966 };
967 resolve_run_style_with_scripts(
968 run_xml,
969 properties,
970 scripts,
971 context,
972 paragraph_style_id,
973 table_run_style,
974 )
975}
976
977pub(crate) fn parse_run_style_from_properties(
978 run_xml: &str,
979 properties: &str,
980 scripts: u8,
981 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
982 paragraph_style_id: Option<&str>,
983 table_run_style: Option<&TextStyle>,
984) -> Option<SharedTextStyle> {
985 resolve_run_style_with_scripts(
986 run_xml,
987 Some(properties),
988 if properties.is_empty() { 0 } else { scripts },
989 context,
990 paragraph_style_id,
991 table_run_style,
992 )
993}
994
995fn resolve_run_style_with_scripts(
996 run_xml: &str,
997 properties: Option<&str>,
998 scripts: u8,
999 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1000 paragraph_style_id: Option<&str>,
1001 table_run_style: Option<&TextStyle>,
1002) -> Option<SharedTextStyle> {
1003 if let Some(xml) = properties {
1004 let cached = RUN_STYLE_CACHE.with(|cache| {
1005 let cache = cache.borrow();
1006 cache
1007 .as_ref()?
1008 .resolved
1009 .get(xml)?
1010 .iter()
1011 .find(|entry| {
1012 entry.scripts == scripts
1013 && entry.paragraph.as_deref() == paragraph_style_id
1014 && same_mark_default_style(entry.table.as_deref(), table_run_style)
1015 })
1016 .map(|entry| entry.style.clone())
1017 });
1018 if let Some(style) = cached {
1019 return style;
1020 }
1021 }
1022 let run_style_id = super::scan::find_attribute_value_in_tag(run_xml, "w:rStyle", "w:val")
1023 .filter(|value| !value.is_empty());
1024 let direct = match properties {
1025 Some(xml) => cached_shared_text_style(xml, &context.styles().theme_fonts, scripts),
1026 None => parse_direct_run_style(run_xml, context).map(SharedTextStyle::from),
1027 };
1028 let inherited_paragraph_run_style = paragraph_style_id
1029 .and_then(|id| context.styles().paragraph_style_by_id.get(id))
1030 .and_then(|style| style.run_style.as_ref());
1031 let inherited_run_style = run_style_id
1032 .as_deref()
1033 .and_then(|id| context.styles().run_style_by_id.get(id));
1034 let default_run_style = super::util::merge_text_style_refs(&[
1035 context.styles().default_run_style.as_ref(),
1036 table_run_style,
1037 ]);
1038
1039 let toggle_of = |get: fn(&TextStyle) -> Option<bool>| {
1040 resolve_toggle_property(
1041 default_run_style.as_ref().and_then(get),
1042 inherited_paragraph_run_style.and_then(get),
1043 inherited_run_style.and_then(get),
1044 direct.as_deref().and_then(get),
1045 )
1046 };
1047 let bold = toggle_of(|s| s.bold);
1048 let italic = toggle_of(|s| s.italic);
1049 let strike = toggle_of(|s| s.strike);
1050 let all_caps = toggle_of(|s| s.all_caps);
1051 let small_caps = toggle_of(|s| s.small_caps);
1052
1053 let merged = super::util::merge_text_style_refs(&[
1054 default_run_style.as_ref(),
1055 inherited_paragraph_run_style,
1056 inherited_run_style,
1057 direct.as_deref(),
1058 ]);
1059 let resolved = merged.map(|mut style| {
1060 style.bold = bold;
1061 style.italic = italic;
1062 style.strike = strike;
1063 style.all_caps = all_caps;
1064 style.small_caps = small_caps;
1065 super::text_effects::resolve(&mut style, &context.styles().theme_colors);
1066 SharedTextStyle::from(style)
1067 });
1068 if let Some(xml) = properties.filter(|xml| xml.len() <= 8192) {
1069 RUN_STYLE_CACHE.with(|cache| {
1070 if let Some(cache) = cache
1071 .borrow_mut()
1072 .as_mut()
1073 .filter(|cache| cache.resolved.len() < 4096)
1074 {
1075 let variants = cache
1076 .resolved
1077 .entry(xml.to_owned())
1078 .or_insert_with(|| Vec::with_capacity(1));
1079 if variants.len() < 32 {
1080 variants.push(ResolvedRunStyle {
1081 scripts,
1082 paragraph: paragraph_style_id.map(str::to_owned),
1083 table: table_run_style.cloned().map(Box::new),
1084 style: resolved.clone(),
1085 });
1086 }
1087 }
1088 });
1089 }
1090 resolved
1091}
1092
1093pub fn parse_paragraph_style(
1094 paragraph_xml: &str,
1095 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1096) -> Option<ParagraphStyle> {
1097 parse_paragraph_style_in_table(paragraph_xml, context, None)
1098}
1099
1100pub fn parse_paragraph_style_in_table(
1101 paragraph_xml: &str,
1102 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1103 table_paragraph_spacing: Option<&ParagraphSpacing>,
1104) -> Option<ParagraphStyle> {
1105 let xml = super::styles::direct_table_property(paragraph_xml, "w:pPr").unwrap_or("");
1106 parse_paragraph_style_from_properties(xml, context, table_paragraph_spacing)
1107 .map(SharedParagraphStyle::into_owned)
1108}
1109
1110pub(crate) fn parse_paragraph_style_from_properties(
1111 xml: &str,
1112 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1113 table_paragraph_spacing: Option<&ParagraphSpacing>,
1114) -> Option<SharedParagraphStyle> {
1115 let cached = RUN_STYLE_CACHE.with(|cache| {
1116 let cache = cache.borrow();
1117 cache
1118 .as_ref()?
1119 .paragraphs
1120 .get(xml)?
1121 .iter()
1122 .find(|(spacing, _)| spacing.as_ref() == table_paragraph_spacing)
1123 .map(|(_, style)| style.clone())
1124 });
1125 if let Some(style) = cached {
1126 return style;
1127 }
1128 let style = parse_paragraph_style_uncached(xml, context, table_paragraph_spacing)
1129 .map(SharedParagraphStyle::from);
1130 if xml.len() <= 8192 {
1131 RUN_STYLE_CACHE.with(|cache| {
1132 if let Some(cache) = cache
1133 .borrow_mut()
1134 .as_mut()
1135 .filter(|cache| cache.paragraphs.len() < 4096)
1136 {
1137 let variants = cache
1138 .paragraphs
1139 .entry(xml.to_owned())
1140 .or_insert_with(|| Vec::with_capacity(1));
1141 if variants.len() < 16 {
1142 variants.push((table_paragraph_spacing.cloned(), style.clone()));
1143 }
1144 }
1145 });
1146 }
1147 style
1148}
1149
1150fn parse_paragraph_style_uncached(
1151 original_paragraph_properties_xml: &str,
1152 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1153 table_paragraph_spacing: Option<&ParagraphSpacing>,
1154) -> Option<ParagraphStyle> {
1155 let paragraph_properties_xml =
1156 crate::xml::without_direct_child_tags(original_paragraph_properties_xml, "w:pPrChange");
1157 let alignment_match =
1158 super::scan::find_attribute_value_in_tag(¶graph_properties_xml, "w:jc", "w:val")
1159 .filter(|value| !value.is_empty());
1160 let p_style_match =
1161 super::scan::find_attribute_value_in_tag(¶graph_properties_xml, "w:pStyle", "w:val")
1162 .filter(|value| !value.is_empty());
1163 let direct_snap_to_grid = parse_on_off_attribute(¶graph_properties_xml, "snapToGrid");
1164 let direct_text_alignment = parse_paragraph_text_alignment_from_xml(¶graph_properties_xml);
1165 let direct_spacing = parse_paragraph_spacing_from_xml(¶graph_properties_xml);
1166 let direct_indent = parse_paragraph_indent_from_xml(¶graph_properties_xml);
1167 let direct_background_color = parse_paragraph_shading_from_xml(¶graph_properties_xml);
1168 let direct_borders = parse_paragraph_border_set_from_xml(¶graph_properties_xml);
1169 let direct_numbering = parse_paragraph_numbering_from_xml(¶graph_properties_xml);
1170 let direct_tab_stops = parse_paragraph_tab_stops_from_xml(¶graph_properties_xml);
1171 let direct_drop_cap = parse_paragraph_drop_cap_from_xml(¶graph_properties_xml);
1172 let direct_contextual_spacing =
1173 parse_on_off_attribute(¶graph_properties_xml, "contextualSpacing");
1174 let direct_keep_next = parse_on_off_attribute(¶graph_properties_xml, "keepNext");
1175 let direct_keep_lines = parse_on_off_attribute(¶graph_properties_xml, "keepLines");
1176 let direct_widow_control = parse_on_off_attribute(¶graph_properties_xml, "widowControl");
1177 let direct_page_break_before =
1178 parse_on_off_attribute(¶graph_properties_xml, "pageBreakBefore");
1179 let has_direct_num_pr =
1180 re::get_unchecked(r"(?i)<w:numPr\b").is_match(¶graph_properties_xml);
1181
1182 let explicit_style_id = p_style_match;
1183 let style_id = explicit_style_id
1184 .clone()
1185 .or_else(|| context.styles().default_paragraph_style_id.clone());
1186 let inherited = style_id
1187 .as_deref()
1188 .and_then(|id| context.styles().paragraph_style_by_id.get(id));
1189 let default_paragraph_style = context.styles().default_paragraph_style.as_ref();
1190
1191 let align = to_model_alignment(alignment_match.as_deref())
1192 .or_else(|| inherited.and_then(|s| s.align))
1193 .or_else(|| default_paragraph_style.and_then(|s| s.align));
1194 let inherited_text_alignment = inherited
1195 .and_then(|style| style.text_alignment)
1196 .or_else(|| default_paragraph_style.and_then(|style| style.text_alignment))
1197 .or(context.styles().default_paragraph_text_alignment);
1198 let text_alignment = direct_text_alignment.or(inherited_text_alignment);
1199 let inherited_snap_to_grid = inherited
1200 .and_then(|style| style.snap_to_grid)
1201 .or_else(|| default_paragraph_style.and_then(|style| style.snap_to_grid));
1202 let snap_to_grid = direct_snap_to_grid.or(inherited_snap_to_grid);
1203 let heading_level = super::styles::parse_heading_level_from_outline(¶graph_properties_xml)
1204 .unwrap_or_else(|| match inherited {
1205 Some(style) => style.heading_level,
1206 None => normalize_heading_level(explicit_style_id.as_deref())
1207 .or_else(|| default_paragraph_style.and_then(|s| s.heading_level)),
1208 });
1209 let numbering = if has_direct_num_pr {
1210 direct_numbering.or_else(|| {
1211 match parse_paragraph_numbering_parts_from_xml(¶graph_properties_xml) {
1216 Some((None, Some(direct_ilvl))) => inherited
1217 .and_then(|s| s.numbering.clone())
1218 .or_else(|| default_paragraph_style.and_then(|s| s.numbering.clone()))
1219 .map(|style_numbering| crate::model::ParagraphNumbering {
1220 num_id: style_numbering.num_id,
1221 ilvl: direct_ilvl.max(0),
1222 }),
1223 _ => None,
1224 }
1225 })
1226 } else {
1227 direct_numbering
1228 .or_else(|| inherited.and_then(|s| s.numbering.clone()))
1229 .or_else(|| default_paragraph_style.and_then(|s| s.numbering.clone()))
1230 };
1231 let style_layer_spacing = if explicit_style_id.is_some() {
1235 merge_paragraph_spacing(
1236 table_paragraph_spacing,
1237 inherited.and_then(|s| s.spacing.as_ref()),
1238 )
1239 } else {
1240 merge_paragraph_spacing(
1241 inherited.and_then(|s| s.spacing.as_ref()),
1242 table_paragraph_spacing,
1243 )
1244 };
1245 let spacing = merge_paragraph_spacing(
1246 merge_paragraph_spacing(
1247 context.styles().doc_default_paragraph_spacing.as_ref(),
1248 style_layer_spacing.as_ref(),
1249 )
1250 .as_ref(),
1251 direct_spacing.as_ref(),
1252 );
1253 let indent = merge_paragraph_indent(
1254 merge_paragraph_indent(
1255 default_paragraph_style.and_then(|s| s.indent.as_ref()),
1256 inherited.and_then(|s| s.indent.as_ref()),
1257 )
1258 .as_ref(),
1259 direct_indent.as_ref(),
1260 );
1261 let background_color = direct_background_color.or_else(|| {
1262 inherited
1263 .and_then(|s| s.background_color.clone())
1264 .or_else(|| default_paragraph_style.and_then(|s| s.background_color.clone()))
1265 });
1266 let borders = merge_paragraph_border_sets(
1267 merge_paragraph_border_sets(
1268 default_paragraph_style.and_then(|s| s.borders.as_ref()),
1269 inherited.and_then(|s| s.borders.as_ref()),
1270 )
1271 .as_ref(),
1272 direct_borders.as_ref(),
1273 );
1274 let tab_stops = merge_paragraph_tab_stops(
1275 merge_paragraph_tab_stops(
1276 default_paragraph_style.and_then(|s| s.tab_stops.as_deref()),
1277 inherited.and_then(|s| s.tab_stops.as_deref()),
1278 )
1279 .as_deref(),
1280 Some(direct_tab_stops.as_slice()),
1281 );
1282 let contextual_spacing = merge_paragraph_boolean(
1283 merge_paragraph_boolean(
1284 default_paragraph_style.and_then(|s| s.contextual_spacing),
1285 inherited.and_then(|s| s.contextual_spacing),
1286 ),
1287 direct_contextual_spacing,
1288 );
1289 let keep_next = merge_paragraph_boolean(
1290 merge_paragraph_boolean(
1291 default_paragraph_style.and_then(|s| s.keep_next),
1292 inherited.and_then(|s| s.keep_next),
1293 ),
1294 direct_keep_next,
1295 );
1296 let keep_lines = merge_paragraph_boolean(
1297 merge_paragraph_boolean(
1298 default_paragraph_style.and_then(|s| s.keep_lines),
1299 inherited.and_then(|s| s.keep_lines),
1300 ),
1301 direct_keep_lines,
1302 );
1303 let widow_control = merge_paragraph_boolean(
1304 merge_paragraph_boolean(
1305 default_paragraph_style.and_then(|s| s.widow_control),
1306 inherited.and_then(|s| s.widow_control),
1307 ),
1308 direct_widow_control,
1309 );
1310 let page_break_before = merge_paragraph_boolean(
1311 merge_paragraph_boolean(
1312 default_paragraph_style.and_then(|s| s.page_break_before),
1313 inherited.and_then(|s| s.page_break_before),
1314 ),
1315 direct_page_break_before,
1316 );
1317 let style_name = inherited.map(|s| s.name.clone());
1318
1319 if align.is_none()
1320 && snap_to_grid.is_none()
1321 && text_alignment.is_none()
1322 && heading_level.is_none()
1323 && style_id.is_none()
1324 && style_name.is_none()
1325 && numbering.is_none()
1326 && spacing.is_none()
1327 && indent.is_none()
1328 && background_color.is_none()
1329 && borders.is_none()
1330 && tab_stops.as_ref().is_none_or(|t| t.is_empty())
1331 && direct_drop_cap.is_none()
1332 && contextual_spacing.is_none()
1333 && keep_next.is_none()
1334 && keep_lines.is_none()
1335 && widow_control.is_none()
1336 && page_break_before.is_none()
1337 {
1338 return None;
1339 }
1340
1341 Some(ParagraphStyle {
1342 align,
1343 snap_to_grid,
1344 source_snap_to_grid: snap_to_grid,
1345 source_has_snap_to_grid: Some(
1346 super::styles::direct_table_property(¶graph_properties_xml, "w:snapToGrid")
1347 .is_some(),
1348 ),
1349 source_inherited_snap_to_grid: inherited_snap_to_grid,
1350 source_table_snap_to_grid: None,
1351 text_alignment,
1352 source_text_alignment: text_alignment,
1353 source_has_text_alignment: Some(
1354 super::styles::direct_table_property(¶graph_properties_xml, "w:textAlignment")
1355 .is_some(),
1356 ),
1357 source_inherited_text_alignment: inherited_text_alignment,
1358 source_table_text_alignment: None,
1359 heading_level,
1360 style_id,
1361 style_name,
1362 numbering,
1363 source_before_auto_spacing: spacing
1364 .as_ref()
1365 .and_then(|spacing| spacing.before_auto_spacing),
1366 source_after_auto_spacing: spacing
1367 .as_ref()
1368 .and_then(|spacing| spacing.after_auto_spacing),
1369 spacing,
1370 indent,
1371 background_color,
1372 borders,
1373 tab_stops,
1374 drop_cap: direct_drop_cap,
1375 contextual_spacing,
1376 keep_next,
1377 keep_lines,
1378 widow_control,
1379 page_break_before,
1380 })
1381}
1382
1383fn regex_capture(xml: &str, pattern: &str) -> Option<String> {
1384 if xml.is_empty() {
1385 return None;
1386 }
1387 re::get(pattern)?
1388 .captures(xml)
1389 .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
1390}
1391
1392#[cfg(test)]
1393mod shared_cache_tests {
1394 #[test]
1395 fn editing_one_cached_style_does_not_change_other_nodes() {
1396 let scope = super::run_style_cache_scope();
1397 let xml = "<w:rPr><w:b/></w:rPr>";
1398 let mut first = super::parse_shared_text_style_from_xml(xml, &Default::default()).unwrap();
1399 let second = super::parse_shared_text_style_from_xml(xml, &Default::default()).unwrap();
1400 drop(scope);
1401 first.bold = Some(false);
1402 assert_eq!(first.bold, Some(false));
1403 assert_eq!(second.bold, Some(true));
1404 }
1405}
1406
1407#[cfg(all(test, not(target_arch = "wasm32")))]
1408mod retained_style_tests {
1409 #[test]
1410 fn raw_style_reuse_preserves_theme_and_script_dependencies() {
1411 let xml = "<w:rPr><w:rFonts w:asciiTheme='minorHAnsi' w:eastAsia='East'/><w:b/><w:sz w:val='24'/></w:rPr>";
1412 for font in ["Serif", "Sans", "Serif"] {
1413 let theme = super::ThemeFontMap {
1414 minor_latin: Some(font.into()),
1415 ..Default::default()
1416 };
1417 for scripts in [0, 1, 2, 128] {
1418 let expected = if scripts == 128 {
1419 crate::parse::styles::parse_definition_style_uncached(xml, &theme)
1420 } else {
1421 super::parse_text_style_with_scripts(xml, &theme, scripts)
1422 };
1423 let _scope = super::run_style_cache_scope();
1424 assert_eq!(super::cached_text_style(xml, &theme, scripts), expected);
1425 }
1426 }
1427 }
1428
1429 #[test]
1430 fn oversized_raw_style_sets_are_not_retained() {
1431 {
1432 let _scope = super::run_style_cache_scope();
1433 let theme = super::ThemeFontMap::default();
1434 for index in 1..=520 {
1435 let xml = format!("<w:rPr><w:sz w:val='{index}'/></w:rPr>");
1436 let actual = super::cached_text_style(&xml, &theme, 0).unwrap();
1437 assert_eq!(actual.font_size_pt, Some(index as f64 / 2.0));
1438 }
1439 }
1440 super::RETAINED_RAW_STYLES.with(|cache| assert!(cache.borrow().is_none()));
1441 }
1442}
1443
1444#[cfg(all(test, not(target_arch = "wasm32")))]
1445mod retained_resolution_tests {
1446 use super::*;
1447 use crate::package::{OoxmlPackage, OoxmlPart};
1448 use std::{cell::RefCell, collections::HashMap, sync::Arc};
1449
1450 #[test]
1451 fn retained_resolution_is_bound_to_an_immutable_sheet() {
1452 let sheets: Vec<_> = ["Alpha", "Beta"].into_iter().enumerate().map(|(index, font)| {
1453 let name = "word/styles.xml".to_owned();
1454 let content = format!(r#"<w:styles><w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="{font}"/><w:b w:val="{index}"/></w:rPr></w:rPrDefault></w:docDefaults><w:style w:type="paragraph" w:default="1" w:styleId="Main"><w:name w:val="{font}"/><w:pPr><w:spacing w:before="{}"/></w:pPr></w:style></w:styles>"#, 120 * (index + 1));
1455 let package = OoxmlPackage { parts: HashMap::from([(name.clone(), OoxmlPart { name, content })]), binary_assets: HashMap::new(), warnings: Vec::new() };
1456 Arc::new(super::super::styles::parse_document_style_sheet(&package))
1457 }).collect();
1458 let parts = HashMap::new();
1459 let assets = HashMap::new();
1460 let run = r#"<w:r><w:rPr><w:i/></w:rPr><w:t>text</w:t></w:r>"#;
1461 let paragraph = r#"<w:p><w:pPr><w:pStyle w:val="Main"/></w:pPr></w:p>"#;
1462 let mut expected_styles = Vec::new();
1463 for index in [0, 0, 1, 1, 0, 1, 0] {
1464 let sheet = &sheets[index];
1465 let context = ParseContext { relationships: HashMap::new(), content_types: Default::default(), parts: &parts, binary_assets: &assets, style_sheet: sheet.as_ref(), warnings: RefCell::new(Vec::new()) };
1466 let expected_run = parse_run_style(run, &context, Some("Main"));
1467 let expected_paragraph = parse_paragraph_style(paragraph, &context);
1468 expected_styles.push((expected_run.clone(), expected_paragraph.clone()));
1469 {
1470 let _scope = run_style_cache_scope();
1471 bind_shared_style_cache(Arc::clone(sheet));
1472 let mut actual_run = parse_run_style(run, &context, Some("Main"));
1473 let mut actual_paragraph = parse_paragraph_style(paragraph, &context);
1474 assert_eq!(actual_run, expected_run);
1475 assert_eq!(actual_paragraph, expected_paragraph);
1476 actual_run.as_mut().unwrap().font_family = Some("changed".to_owned());
1477 actual_paragraph.as_mut().unwrap().style_name = Some("changed".to_owned());
1478 }
1479 {
1480 let _scope = run_style_cache_scope();
1481 bind_shared_style_cache(Arc::clone(sheet));
1482 assert_eq!(parse_run_style(run, &context, Some("Main")), expected_run);
1483 assert_eq!(parse_paragraph_style(paragraph, &context), expected_paragraph);
1484 {
1485 let _nested = run_style_cache_scope();
1486 bind_shared_style_cache(Arc::clone(&sheets[1 - index]));
1487 }
1488 assert_eq!(parse_run_style(run, &context, Some("Main")), expected_run);
1489 }
1490 }
1491 assert_ne!(expected_styles[0], expected_styles[2]);
1492 }
1493}
1494
1495#[cfg(all(test, not(target_arch = "wasm32")))]
1496mod retained_numeric_style_tests {
1497 use super::*;
1498 use std::{cell::RefCell, collections::HashMap, sync::Arc};
1499
1500 #[test]
1501 fn retained_table_formats_preserve_float_bits() {
1502 let sheet = Arc::new(super::super::context::ParsedStyleSheet::empty());
1503 let parts = HashMap::new();
1504 let assets = HashMap::new();
1505 let context = ParseContext { relationships: HashMap::new(), content_types: Default::default(), parts: &parts, binary_assets: &assets, style_sheet: sheet.as_ref(), warnings: RefCell::new(Vec::new()) };
1506 for value in [0.0_f64, -0.0_f64, 0.0_f64, -0.0_f64] {
1507 let mut table = parse_text_style_from_xml("<w:b/>", &Default::default()).unwrap();
1508 table.font_size_pt = Some(value);
1509 let _scope = run_style_cache_scope();
1510 bind_shared_style_cache(Arc::clone(&sheet));
1511 let actual = parse_run_style_in_table("<w:r><w:t>text</w:t></w:r>", &context, None, Some(&table)).unwrap();
1512 assert_eq!(actual.font_size_pt.unwrap().to_bits(), value.to_bits());
1513 }
1514 }
1515}
1516
1517#[cfg(test)]
1518mod drawing_style_preflight_tests {
1519 use super::*;
1520
1521 #[test]
1522 fn preflight_keeps_text_styles_and_ignores_geometry_only_content() {
1523 for xml in [
1524 r#"<a:rPr b="1"/>"#,
1525 r#"<A:RPR sz="1200"/>"#,
1526 r#"<a:defRPr><a:latin typeface="Primary"/></a:defRPr>"#,
1527 r#"<a:latin typeface="Primary"/>"#,
1528 r#"<![CDATA[<a:rPr b="1"/>]]>"#,
1529 r#"<a:rPr-extra b="1"/>"#,
1530 ] {
1531 assert!(has_drawing_text_style_tags(xml));
1532 assert!(parse_text_style_from_xml(xml, &Default::default()).is_some());
1533 }
1534 for xml in [
1535 "", "<a:", "<a:pt x=\"1\" y=\"2\"/>",
1536 "<a:rPrExtra/>", "<a:ln><a:noFill/></a:ln>",
1537 "<a:solidFill><a:srgbClr val=\"ffffff\"/></a:solidFill>",
1538 ] {
1539 assert!(parse_text_style_from_xml(xml, &Default::default()).is_none());
1540 }
1541 let xml = r#"<w:r><w:rPr><w:b/></w:rPr><a:pt x="1" y="2"/></w:r>"#;
1542 assert!(!has_drawing_text_style_tags(xml));
1543 assert_eq!(parse_text_style_from_xml(xml, &Default::default()).unwrap().bold, Some(true));
1544 }
1545}
1546
1547#[cfg(all(test, not(target_arch = "wasm32")))]
1548mod bound_mark_default_tests {
1549 use super::*;
1550 use std::sync::Arc;
1551
1552 #[test]
1553 fn bound_defaults_preserve_values_and_validate_unbound_inputs() {
1554 let mut defaults = parse_text_style_from_xml("<w:b/>", &Default::default()).unwrap();
1555 defaults.font_size_pt = Some(-0.0);
1556 let mut sheet = super::super::context::ParsedStyleSheet::empty();
1557 sheet.default_run_style = Some(defaults.clone());
1558 let sheet = Arc::new(sheet);
1559 let _scope = run_style_cache_scope();
1560 bind_shared_style_cache(Arc::clone(&sheet));
1561 for _ in 0..2 {
1562 let actual = cached_mark_default_style(sheet.default_run_style.as_ref(), None).unwrap();
1563 assert_eq!(*actual, defaults);
1564 assert_eq!(actual.font_size_pt.unwrap().to_bits(), (-0.0_f64).to_bits());
1565 }
1566 for value in [0.0_f64, -0.0_f64] {
1567 defaults.font_size_pt = Some(value);
1568 let actual = cached_mark_default_style(Some(&defaults), None).unwrap();
1569 assert_eq!(actual.font_size_pt.unwrap().to_bits(), value.to_bits());
1570 }
1571 defaults.bold = Some(false);
1572 assert_eq!(cached_mark_default_style(Some(&defaults), None).unwrap().bold, Some(false));
1573 assert_eq!(cached_mark_default_style(sheet.default_run_style.as_ref(), None).unwrap().bold, Some(true));
1574 let table = parse_text_style_from_xml("<w:i/>", &Default::default()).unwrap();
1575 assert_eq!(cached_mark_default_style(sheet.default_run_style.as_ref(), Some(&table)).unwrap().italic, Some(true));
1576 assert_eq!(cached_mark_default_style(sheet.default_run_style.as_ref(), None).unwrap().italic, None);
1577 bind_shared_style_cache(Arc::new(super::super::context::ParsedStyleSheet::empty()));
1578 assert!(cached_mark_default_style(None, None).is_none());
1579 }
1580}
1581
1582#[cfg(test)]
1583mod shared_paragraph_cache_tests {
1584 use super::*;
1585 use std::{cell::RefCell, collections::HashMap};
1586
1587 #[test]
1588 fn shared_paragraph_styles_preserve_cached_values_and_serialization() {
1589 let parts = HashMap::new();
1590 let assets = HashMap::new();
1591 let sheet = super::super::context::ParsedStyleSheet::empty();
1592 let context = ParseContext { relationships: HashMap::new(), content_types: Default::default(), parts: &parts, binary_assets: &assets, style_sheet: &sheet, warnings: RefCell::new(Vec::new()) };
1593 let _scope = run_style_cache_scope();
1594 let properties = "<w:pPr><w:jc w:val='center'/></w:pPr>";
1595 let mut first = parse_paragraph_style_from_properties(properties, &context, None).unwrap();
1596 let second = parse_paragraph_style_from_properties(properties, &context, None).unwrap();
1597 assert_eq!(serde_json::to_value(&second).unwrap(), serde_json::to_value(&*second).unwrap());
1598 first.style_name = Some("changed".into());
1599 assert!(second.style_name.is_none());
1600 assert_eq!(parse_paragraph_style_from_properties(properties, &context, None).unwrap(), second);
1601 let mut node: crate::model::ParagraphNode = serde_json::from_str(r#"{"type":"paragraph","children":[],"style":{"styleName":"base"}}"#).unwrap();
1602 let unchanged = node.clone();
1603 node.style.as_mut().unwrap().style_name = Some("changed".into());
1604 assert_eq!(unchanged.style.as_ref().unwrap().style_name.as_deref(), Some("base"));
1605 }
1606}