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