1use rpptx_oxml::placeholder::PhType;
4use rpptx_oxml::shape_tree::CT_Shape;
5
6use oxml_drawing::fill::{Fill, SolidFill};
7use oxml_drawing::text::{
8 CT_TextCharacterProperties, CT_TextListStyle, CT_TextParagraphProperties,
9};
10
11use crate::ResolveCtx;
12
13#[derive(Clone, Debug, Default, Eq, PartialEq)]
15pub struct EffectiveTextProperties {
16 pub paragraph: CT_TextParagraphProperties,
17 pub run: CT_TextCharacterProperties,
18 pub language: Option<String>,
20}
21
22#[derive(Clone, Debug, Default, Eq, PartialEq)]
24pub struct EffectiveListStyle {
25 levels: [EffectiveTextProperties; 9],
26}
27
28impl EffectiveListStyle {
29 pub fn level(&self, level: u8) -> Option<&EffectiveTextProperties> {
31 self.levels.get(usize::from(level))
32 }
33}
34
35impl ResolveCtx<'_> {
36 pub fn effective_list_style(&self, shape: &CT_Shape) -> EffectiveListStyle {
38 let key = shape
39 .placeholder
40 .as_ref()
41 .map(|placeholder| placeholder.key());
42 let mut effective = if let Some(cached) = self.list_style_cache.borrow().get(&key) {
43 cached.clone()
44 } else {
45 let resolved = self.resolve_list_style_prefix(shape);
46 self.list_style_cache
47 .borrow_mut()
48 .insert(key, resolved.clone());
49 resolved
50 };
51 if let Some(color) = shape
55 .style()
56 .and_then(|style| style.font_reference.color.as_ref())
57 .filter(|color| self.concrete_color(color).is_ok())
58 {
59 let mut solid = SolidFill::default();
60 solid.color = Some(color.clone());
61 for level in &mut effective.levels {
62 level.run.fill = Some(Fill::Solid(solid.clone()));
63 }
64 }
65 if let Some(list_style) = shape.text_body.as_ref().and_then(|body| body.list_style()) {
66 merge_list_style(&mut effective, list_style);
67 }
68 effective
69 }
70
71 pub fn effective_text_properties(
73 &self,
74 shape: &CT_Shape,
75 paragraph: Option<&CT_TextParagraphProperties>,
76 run: Option<&CT_TextCharacterProperties>,
77 ) -> EffectiveTextProperties {
78 let style = self.effective_list_style(shape);
79 let level = paragraph
80 .and_then(|properties| properties.level)
81 .unwrap_or(0);
82 let mut effective = style
83 .level(level)
84 .cloned()
85 .unwrap_or_else(EffectiveTextProperties::default);
86 if let Some(properties) = paragraph {
87 merge_paragraph(&mut effective, properties);
88 }
89 if let Some(properties) = run {
90 merge_character(&mut effective, properties);
91 }
92 effective
93 }
94
95 pub(crate) fn effective_table_text_properties(
97 &self,
98 table_style: Option<&CT_TextCharacterProperties>,
99 paragraph: Option<&CT_TextParagraphProperties>,
100 run: Option<&CT_TextCharacterProperties>,
101 ) -> EffectiveTextProperties {
102 let mut style = EffectiveListStyle::default();
103 merge_list_style(&mut style, self.default_text_style);
104 if let Some(styles) = &self.master.text_styles {
105 merge_list_style(&mut style, &styles.other_style);
106 }
107 let level = paragraph
108 .and_then(|properties| properties.level)
109 .unwrap_or(0);
110 let mut effective = style
111 .level(level)
112 .cloned()
113 .unwrap_or_else(EffectiveTextProperties::default);
114 if let Some(properties) = table_style {
115 merge_character(&mut effective, properties);
116 }
117 if let Some(properties) = paragraph {
118 merge_paragraph(&mut effective, properties);
119 }
120 if let Some(properties) = run {
121 merge_character(&mut effective, properties);
122 }
123 effective
124 }
125
126 fn resolve_list_style_prefix(&self, shape: &CT_Shape) -> EffectiveListStyle {
127 let mut effective = EffectiveListStyle::default();
128 merge_list_style(&mut effective, self.default_text_style);
129
130 if let Some(styles) = &self.master.text_styles {
131 let master_style = match shape
132 .placeholder
133 .as_ref()
134 .map(|placeholder| placeholder.effective_type())
135 {
136 Some(PhType::Title | PhType::CenteredTitle) => &styles.title_style,
137 Some(PhType::Body | PhType::Subtitle | PhType::Object) => &styles.body_style,
138 _ => &styles.other_style,
139 };
140 merge_list_style(&mut effective, master_style);
141 }
142
143 let (layout_placeholder, master_placeholder) = self.placeholder_chain(shape);
144 if let Some(list_style) = master_placeholder
145 .and_then(|placeholder| placeholder.text_body.as_ref())
146 .and_then(|body| body.list_style())
147 {
148 merge_list_style(&mut effective, list_style);
149 }
150 if let Some(list_style) = layout_placeholder
151 .and_then(|placeholder| placeholder.text_body.as_ref())
152 .and_then(|body| body.list_style())
153 {
154 merge_list_style(&mut effective, list_style);
155 }
156 effective
157 }
158}
159
160fn merge_list_style(effective: &mut EffectiveListStyle, source: &CT_TextListStyle) {
161 for level in 0..9 {
162 if let Some(properties) = &source.default_paragraph_properties {
163 merge_paragraph(&mut effective.levels[level], properties);
164 }
165 if let Some(properties) = source.level(level + 1) {
166 merge_paragraph(&mut effective.levels[level], properties);
167 }
168 }
169}
170
171fn merge_paragraph(effective: &mut EffectiveTextProperties, source: &CT_TextParagraphProperties) {
172 if source.left_margin.is_some() {
173 effective.paragraph.left_margin = source.left_margin;
174 }
175 if source.right_margin.is_some() {
176 effective.paragraph.right_margin = source.right_margin;
177 }
178 if source.level.is_some() {
179 effective.paragraph.level = source.level;
180 }
181 if source.indent.is_some() {
182 effective.paragraph.indent = source.indent;
183 }
184 if source.alignment.is_some() {
185 effective.paragraph.alignment = source.alignment;
186 }
187 if source.right_to_left.is_some() {
188 effective.paragraph.right_to_left = source.right_to_left;
189 }
190 if source.line_spacing.is_some() {
191 effective.paragraph.line_spacing = source.line_spacing.clone();
192 }
193 if source.space_before.is_some() {
194 effective.paragraph.space_before = source.space_before.clone();
195 }
196 if source.space_after.is_some() {
197 effective.paragraph.space_after = source.space_after.clone();
198 }
199 if let Some(source_bullet) = &source.bullet {
200 let bullet = effective.paragraph.bullet.get_or_insert_default();
201 if source_bullet.color.is_some() {
202 bullet.color = source_bullet.color.clone();
203 }
204 if source_bullet.size.is_some() {
205 bullet.size = source_bullet.size.clone();
206 }
207 if source_bullet.font.is_some() {
208 bullet.font = source_bullet.font.clone();
209 }
210 if source_bullet.choice.is_some() {
211 bullet.choice = source_bullet.choice.clone();
212 }
213 }
214 if let Some(properties) = &source.default_run_properties {
215 merge_character(effective, properties);
216 }
217}
218
219fn merge_character(effective: &mut EffectiveTextProperties, source: &CT_TextCharacterProperties) {
220 if let Some(language) = source.language() {
221 effective.language = Some(language.to_owned());
222 }
223 merge_character_properties(&mut effective.run, source);
224}
225
226fn merge_character_properties(
227 effective: &mut CT_TextCharacterProperties,
228 source: &CT_TextCharacterProperties,
229) {
230 if source.font_size.is_some() {
231 effective.font_size = source.font_size;
232 }
233 if source.bold.is_some() {
234 effective.bold = source.bold;
235 }
236 if source.italic.is_some() {
237 effective.italic = source.italic;
238 }
239 if source.all_caps == Some(false) && source.small_caps == Some(false) {
240 effective.all_caps = Some(false);
242 effective.small_caps = Some(false);
243 } else {
244 if source.all_caps.is_some() {
245 effective.set_all_caps(source.all_caps);
246 }
247 if source.small_caps.is_some() {
248 effective.set_small_caps(source.small_caps);
249 }
250 }
251 if source.highlight.is_some() {
252 effective.highlight = source.highlight.clone();
253 }
254 if source.underline.is_some() {
255 effective.underline = source.underline;
256 }
257 if source.strike.is_some() {
258 effective.strike = source.strike;
259 }
260 if source.spacing.is_some() {
261 effective.spacing = source.spacing.clone();
262 }
263 if source.baseline.is_some() {
264 effective.baseline = source.baseline.clone();
265 }
266 if source.fill.is_some() {
267 effective.fill = source.fill.clone();
268 }
269 if source.latin.is_some() {
270 effective.latin = source.latin.clone();
271 }
272 if source.east_asian.is_some() {
273 effective.east_asian = source.east_asian.clone();
274 }
275 if source.complex_script.is_some() {
276 effective.complex_script = source.complex_script.clone();
277 }
278 if source.symbol.is_some() {
279 effective.symbol = source.symbol.clone();
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use oxml_drawing::text::{
286 CT_TextCharacterProperties, CT_TextListStyle, CT_TextParagraphProperties, TextAlignment,
287 TextBulletChoice, TextBulletSizeValue,
288 };
289 use oxml_drawing::{color::ColorMap, theme::CT_OfficeStyleSheet};
290 use rpptx_oxml::shape_tree::{CT_Shape, ShapeTreeChild};
291 use rpptx_oxml::slide_parts::{CT_Slide, CT_SlideLayout, CT_SlideMaster};
292
293 use super::{EffectiveListStyle, EffectiveTextProperties, merge_list_style, merge_paragraph};
294 use crate::ResolveCtx;
295
296 const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
297
298 #[test]
299 fn seven_source_list_style_merge_resolves_run_size_and_typeface() {
300 let fixture = Fixture::new(
301 "<a:defPPr marL=\"100\"><a:defRPr sz=\"1000\"/></a:defPPr>",
302 "<a:defPPr marR=\"200\"/>",
303 "<a:defPPr indent=\"300\"/>",
304 "<a:defPPr algn=\"ctr\"/>",
305 &["<a:defPPr><a:defRPr b=\"1\"/></a:defPPr>"],
306 );
307 let paragraph = paragraph("<a:pPr><a:defRPr sz=\"2400\"/></a:pPr>");
308 let run = character("<a:rPr><a:latin typeface=\"Run Face\"/></a:rPr>");
309 let resolved = fixture.context().effective_text_properties(
310 fixture.slide_shape(0),
311 Some(¶graph),
312 Some(&run),
313 );
314
315 assert_eq!(resolved.run.font_size, Some(2400));
316 assert_eq!(resolved.run.latin.unwrap().typeface, "Run Face");
317 assert_eq!(resolved.paragraph.left_margin, Some(100));
318 assert_eq!(resolved.paragraph.right_margin, Some(200));
319 assert_eq!(resolved.paragraph.indent, Some(300));
320 assert_eq!(resolved.paragraph.alignment, Some(TextAlignment::Center));
321 assert_eq!(resolved.run.bold, Some(true));
322 }
323
324 #[test]
325 fn default_paragraph_properties_apply_before_each_level() {
326 let source = list_style(
327 "<a:defPPr marL=\"100\"><a:defRPr sz=\"1200\"/></a:defPPr>\
328 <a:lvl1pPr marL=\"200\"/><a:lvl9pPr><a:defRPr b=\"1\"/></a:lvl9pPr>",
329 );
330 let mut effective = EffectiveListStyle::default();
331 merge_list_style(&mut effective, &source);
332
333 assert_eq!(effective.level(0).unwrap().paragraph.left_margin, Some(200));
334 assert_eq!(effective.level(8).unwrap().paragraph.left_margin, Some(100));
335 assert_eq!(effective.level(8).unwrap().run.font_size, Some(1200));
336 assert_eq!(effective.level(8).unwrap().run.bold, Some(true));
337 }
338
339 #[test]
340 fn all_nine_levels_merge_independently() {
341 let levels = (1..=9)
342 .map(|level| format!("<a:lvl{level}pPr marL=\"{}\"/>", level * 100))
343 .collect::<String>();
344 let mut effective = EffectiveListStyle::default();
345 merge_list_style(&mut effective, &list_style(&levels));
346
347 for level in 0..9 {
348 assert_eq!(
349 effective.level(level).unwrap().paragraph.left_margin,
350 Some(i32::from(level + 1) * 100)
351 );
352 }
353 }
354
355 #[test]
356 fn later_sources_win_per_property_without_erasing_other_fields() {
357 let mut effective = EffectiveTextProperties::default();
358 merge_paragraph(
359 &mut effective,
360 ¶graph("<a:pPr marL=\"100\" marR=\"200\"><a:defRPr sz=\"1100\" b=\"1\"/></a:pPr>"),
361 );
362 merge_paragraph(
363 &mut effective,
364 ¶graph("<a:pPr marL=\"300\"><a:defRPr sz=\"2200\"/></a:pPr>"),
365 );
366
367 assert_eq!(effective.paragraph.left_margin, Some(300));
368 assert_eq!(effective.paragraph.right_margin, Some(200));
369 assert_eq!(effective.run.font_size, Some(2200));
370 assert_eq!(effective.run.bold, Some(true));
371 }
372
373 #[test]
374 fn paragraph_direction_inherits_and_direct_ltr_overrides_it() {
375 let mut effective = EffectiveTextProperties::default();
376 merge_paragraph(&mut effective, ¶graph("<a:pPr rtl=\"1\"></a:pPr>"));
377 assert_eq!(effective.paragraph.right_to_left, Some(true));
378
379 merge_paragraph(&mut effective, ¶graph("<a:pPr rtl=\"0\"></a:pPr>"));
380 assert_eq!(effective.paragraph.right_to_left, Some(false));
381 }
382
383 #[test]
384 fn all_caps_inherits_and_a_direct_none_value_overrides_it() {
385 let fixture = Fixture::new(
386 "<a:defPPr><a:defRPr cap=\"all\"/></a:defPPr>",
387 "",
388 "",
389 "",
390 &[""],
391 );
392 let inherited =
393 fixture
394 .context()
395 .effective_text_properties(fixture.slide_shape(0), None, None);
396 assert_eq!(inherited.run.all_caps, Some(true));
397
398 let direct = character("<a:rPr cap=\"none\"></a:rPr>");
399 let overridden = fixture.context().effective_text_properties(
400 fixture.slide_shape(0),
401 None,
402 Some(&direct),
403 );
404 assert_eq!(overridden.run.all_caps, Some(false));
405 assert_eq!(overridden.run.small_caps, Some(false));
406 for inherited_small in [false, true] {
407 let mut effective = CT_TextCharacterProperties::default();
408 if inherited_small {
409 effective.set_small_caps(Some(true));
410 } else {
411 effective.set_all_caps(Some(true));
412 }
413 let source_before = direct.clone();
414 super::merge_character_properties(&mut effective, &direct);
415 assert_eq!(effective.all_caps, Some(false));
416 assert_eq!(effective.small_caps, Some(false));
417 assert_eq!(direct, source_before);
418 let mut paragraph = CT_TextParagraphProperties::default();
419 paragraph.default_run_properties = Some(effective);
420 let bytes = paragraph.to_xml().unwrap();
421 let reopened = CT_TextParagraphProperties::from_xml(&bytes)
422 .unwrap()
423 .default_run_properties
424 .unwrap();
425 assert_eq!(reopened.all_caps, Some(false));
426 assert_eq!(reopened.small_caps, Some(false));
427 }
428 }
429
430 #[test]
431 fn fills_and_typeface_slots_merge_as_atomic_properties() {
432 let mut effective = EffectiveTextProperties::default();
433 merge_paragraph(
434 &mut effective,
435 ¶graph(
436 "<a:pPr><a:defRPr><a:solidFill><a:srgbClr val=\"112233\"/></a:solidFill><a:latin typeface=\"Latin One\"/><a:ea typeface=\"East Asian One\"/></a:defRPr></a:pPr>",
437 ),
438 );
439 merge_paragraph(
440 &mut effective,
441 ¶graph(
442 "<a:pPr><a:defRPr><a:solidFill><a:srgbClr val=\"AABBCC\"/></a:solidFill><a:latin typeface=\"Latin Two\"/></a:defRPr></a:pPr>",
443 ),
444 );
445
446 assert_eq!(effective.run.latin.unwrap().typeface, "Latin Two");
447 assert_eq!(effective.run.east_asian.unwrap().typeface, "East Asian One");
448 assert_eq!(
449 effective.run.fill,
450 character("<a:rPr><a:solidFill><a:srgbClr val=\"AABBCC\"/></a:solidFill></a:rPr>").fill
451 );
452 }
453
454 #[test]
455 fn raw_xml_and_hyperlink_actions_are_not_inherited() {
456 let source = paragraph(
457 "<a:pPr producer=\"raw\"><x:opaque xmlns:x=\"urn:x\"/><a:defRPr><a:hlinkClick action=\"ppaction://jump\"/></a:defRPr></a:pPr>",
458 );
459 let mut effective = EffectiveTextProperties::default();
460 merge_paragraph(&mut effective, &source);
461
462 assert!(effective.paragraph.raw_children().is_empty());
463 assert!(effective.run.raw_children().is_empty());
464 assert!(effective.run.hyperlink_click.is_none());
465 assert!(effective.run.hyperlink_mouse_over.is_none());
466 }
467
468 #[test]
469 fn bullet_components_merge_independently() {
470 let mut effective = EffectiveTextProperties::default();
471 merge_paragraph(
472 &mut effective,
473 ¶graph(
474 "<a:pPr><a:buClr><a:srgbClr val=\"112233\"/></a:buClr><a:buSzPts val=\"1800\"/><a:buChar char=\"•\"/></a:pPr>",
475 ),
476 );
477 merge_paragraph(
478 &mut effective,
479 ¶graph("<a:pPr><a:buFont typeface=\"Wingdings\"/><a:buNone/></a:pPr>"),
480 );
481 let bullet = effective.paragraph.bullet.unwrap();
482
483 assert!(bullet.color.is_some());
484 assert!(matches!(
485 bullet.size.unwrap().value,
486 TextBulletSizeValue::Points(1800)
487 ));
488 assert_eq!(bullet.font.unwrap().typeface, "Wingdings");
489 assert!(matches!(bullet.choice, Some(TextBulletChoice::None(_))));
490 }
491
492 #[test]
493 fn shape_owned_style_is_not_shared_by_placeholder_cache() {
494 let fixture = Fixture::new(
495 "",
496 "",
497 "<a:lvl1pPr marL=\"100\"/>",
498 "",
499 &["<a:lvl1pPr marL=\"900\"/>", "<a:lvl1pPr marL=\"800\"/>"],
500 );
501 let context = fixture.context();
502 let first_shape = context.effective_list_style(fixture.slide_shape(0));
503 let second_shape = context.effective_list_style(fixture.slide_shape(1));
504
505 assert_eq!(
506 first_shape.level(0).unwrap().paragraph.left_margin,
507 Some(900)
508 );
509 assert_eq!(
510 second_shape.level(0).unwrap().paragraph.left_margin,
511 Some(800)
512 );
513 }
514
515 #[test]
516 fn style_font_colour_overrides_inherited_styles_under_the_shape_list_style() {
517 let colour = |value: &str| {
518 format!("<a:defRPr><a:solidFill><a:srgbClr val=\"{value}\"/></a:solidFill></a:defRPr>")
519 };
520 let mut fixture = Fixture::new(
521 "",
522 &format!("<a:lvl1pPr>{}</a:lvl1pPr>", colour("111111")),
523 "",
524 "",
525 &[],
526 );
527 let style = r#"<p:style><a:lnRef idx="1"><a:schemeClr val="accent1"/></a:lnRef><a:fillRef idx="3"><a:schemeClr val="accent1"/></a:fillRef><a:effectRef idx="2"><a:schemeClr val="accent1"/></a:effectRef><a:fontRef idx="minor"><a:srgbClr val="222222"/></a:fontRef></p:style>"#;
528 let styled = |list_style: &str| {
529 shape(7, list_style).replace(
530 "<p:spPr/>",
531 &format!(r#"<p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="914400" cy="914400"/></a:xfrm></p:spPr>{style}"#),
532 )
533 };
534 let shapes = [
535 shape(7, ""),
536 styled(""),
537 styled(&format!("<a:lvl1pPr>{}</a:lvl1pPr>", colour("333333"))),
538 styled("").replace(
539 r#"<a:srgbClr val="222222"/></a:fontRef>"#,
540 r#"<a:schemeClr val="phClr"/></a:fontRef>"#,
541 ),
542 styled("").replace(
543 r#"<a:srgbClr val="222222"/></a:fontRef>"#,
544 r#"<a:prstClr val="red"/></a:fontRef>"#,
545 ),
546 ];
547 fixture.slide = CT_Slide::from_xml(slide_xml(&shapes.concat()).as_bytes()).unwrap();
548 let context = fixture.context();
549 let run_fill = |index| {
550 context
551 .effective_text_properties(fixture.slide_shape(index), None, None)
552 .run
553 .fill
554 };
555 let expected = |value: &str| {
556 character(&format!(
557 "<a:rPr><a:solidFill><a:srgbClr val=\"{value}\"/></a:solidFill></a:rPr>"
558 ))
559 .fill
560 };
561
562 assert_eq!(run_fill(0), expected("111111"));
563 assert_eq!(run_fill(1), expected("222222"));
564 assert_eq!(run_fill(2), expected("333333"));
565 assert_eq!(run_fill(3), expected("111111"));
567 assert_eq!(run_fill(4), expected("111111"));
568 context
569 .resolve_slide((720.0, 540.0))
570 .expect("an unresolvable font colour does not fail the slide");
571 }
572
573 #[test]
574 fn default_paragraph_properties_round_trip_in_schema_order() {
575 let xml = format!(
576 "<a:lstStyle xmlns:a=\"{A_NS}\"><x:before xmlns:x=\"urn:x\"/><a:defPPr marL=\"100\"/><x:between xmlns:x=\"urn:x\"/><a:lvl1pPr/><x:after xmlns:x=\"urn:x\"/></a:lstStyle>"
577 );
578 let unqualified = xml.replace("<a:defPPr", "<defPPr");
579 let opaque = CT_TextListStyle::from_xml(unqualified.as_bytes()).unwrap();
580 assert!(opaque.default_paragraph_properties.is_none());
581 assert!(
582 String::from_utf8(opaque.to_xml().unwrap())
583 .unwrap()
584 .contains("<defPPr marL=\"100\"/>")
585 );
586 let style = CT_TextListStyle::from_xml(xml.as_bytes()).unwrap();
587 assert_eq!(
588 style
589 .default_paragraph_properties
590 .as_ref()
591 .unwrap()
592 .left_margin,
593 Some(100)
594 );
595 let output = String::from_utf8(style.to_xml().unwrap()).unwrap();
596
597 assert_eq!(
598 output,
599 "<a:lstStyle xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\"><x:before xmlns:x=\"urn:x\"/><a:defPPr marL=\"100\"/><x:between xmlns:x=\"urn:x\"/><a:lvl1pPr/><x:after xmlns:x=\"urn:x\"/></a:lstStyle>"
600 );
601 }
602
603 fn list_style(children: &str) -> CT_TextListStyle {
604 CT_TextListStyle::from_xml(
605 format!("<a:lstStyle xmlns:a=\"{A_NS}\">{children}</a:lstStyle>").as_bytes(),
606 )
607 .unwrap()
608 }
609
610 fn paragraph(xml: &str) -> CT_TextParagraphProperties {
611 CT_TextParagraphProperties::from_xml(with_namespace(xml).as_bytes()).unwrap()
612 }
613
614 fn character(xml: &str) -> CT_TextCharacterProperties {
615 CT_TextCharacterProperties::from_xml(with_namespace(xml).as_bytes()).unwrap()
616 }
617
618 fn with_namespace(xml: &str) -> String {
619 xml.replacen('>', &format!(" xmlns:a=\"{A_NS}\">"), 1)
620 }
621
622 struct Fixture {
623 theme: CT_OfficeStyleSheet,
624 master: CT_SlideMaster,
625 layout: CT_SlideLayout,
626 slide: CT_Slide,
627 default_text_style: CT_TextListStyle,
628 }
629
630 impl Fixture {
631 fn new(
632 default_style: &str,
633 master_style: &str,
634 master_placeholder: &str,
635 layout_placeholder: &str,
636 slide_styles: &[&str],
637 ) -> Self {
638 let slide_shapes = slide_styles
639 .iter()
640 .map(|style| shape(7, style))
641 .collect::<String>();
642 Self {
643 theme: CT_OfficeStyleSheet::office_default(),
644 master: CT_SlideMaster::from_xml(
645 master_xml(&shape(7, master_placeholder), master_style).as_bytes(),
646 )
647 .unwrap(),
648 layout: CT_SlideLayout::from_xml(
649 layout_xml(&shape(7, layout_placeholder)).as_bytes(),
650 )
651 .unwrap(),
652 slide: CT_Slide::from_xml(slide_xml(&slide_shapes).as_bytes()).unwrap(),
653 default_text_style: list_style(default_style),
654 }
655 }
656
657 fn context(&self) -> ResolveCtx<'_> {
658 ResolveCtx::new(
659 &self.theme,
660 ColorMap::default(),
661 &self.master,
662 &self.layout,
663 &self.slide,
664 &self.default_text_style,
665 )
666 }
667
668 fn slide_shape(&self, index: usize) -> &CT_Shape {
669 let ShapeTreeChild::Shape(shape) =
670 &self.slide.common_slide_data.shape_tree.children[index]
671 else {
672 panic!("expected slide shape");
673 };
674 shape
675 }
676 }
677
678 fn slide_xml(shapes: &str) -> String {
679 format!(
680 "<p:sld xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\" xmlns:a=\"{A_NS}\"><p:cSld>{}</p:cSld></p:sld>",
681 shape_tree(shapes)
682 )
683 }
684
685 fn layout_xml(shapes: &str) -> String {
686 format!(
687 "<p:sldLayout xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\" xmlns:a=\"{A_NS}\"><p:cSld>{}</p:cSld></p:sldLayout>",
688 shape_tree(shapes)
689 )
690 }
691
692 fn master_xml(shapes: &str, body_style: &str) -> String {
693 format!(
694 "<p:sldMaster xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\" xmlns:a=\"{A_NS}\"><p:cSld>{}</p:cSld><p:clrMap bg1=\"lt1\" tx1=\"dk1\" bg2=\"lt2\" tx2=\"dk2\" accent1=\"accent1\" accent2=\"accent2\" accent3=\"accent3\" accent4=\"accent4\" accent5=\"accent5\" accent6=\"accent6\" hlink=\"hlink\" folHlink=\"folHlink\"/><p:txStyles><p:titleStyle/><p:bodyStyle>{body_style}</p:bodyStyle><p:otherStyle/></p:txStyles></p:sldMaster>",
695 shape_tree(shapes)
696 )
697 }
698
699 fn shape_tree(shapes: &str) -> String {
700 format!("<p:spTree><p:nvGrpSpPr/><p:grpSpPr/>{shapes}</p:spTree>")
701 }
702
703 fn shape(index: u32, list_style: &str) -> String {
704 format!(
705 "<p:sp><p:nvSpPr><p:cNvPr/><p:cNvSpPr/><p:nvPr><p:ph type=\"body\" idx=\"{index}\"/></p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle>{list_style}</a:lstStyle><a:p/></p:txBody></p:sp>"
706 )
707 }
708 #[test]
709 fn fx206_language_inherits_by_occurrence_without_promoting_raw_source() {
710 let raw = br#"<q:rPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:foreign" lang="tr-TR" x:lang="zz" x:kept="1"/>"#;
711 let props = CT_TextCharacterProperties::from_xml(raw).unwrap();
712 let before = props.clone();
713 assert_eq!(props.language(), Some("tr-TR"));
714 assert_eq!(props, before);
715 let mut effective = EffectiveTextProperties::default();
716 super::merge_character(&mut effective, &props);
717 assert_eq!(effective.language.as_deref(), Some("tr-TR"));
718 let absent = CT_TextCharacterProperties::from_xml(br#"<a:rPr/>"#).unwrap();
719 super::merge_character(&mut effective, &absent);
720 assert_eq!(effective.language.as_deref(), Some("tr-TR"));
721 let own = CT_TextCharacterProperties::from_xml(br#"<a:rPr lang="en-US"/>"#).unwrap();
722 super::merge_character(&mut effective, &own);
723 assert_eq!(effective.language.as_deref(), Some("en-US"));
724 assert_eq!(props, before);
725 }
726}