Skip to main content

rpptx_layout/
text.rs

1//! Text inheritance resolution.
2
3use 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/// Resolved paragraph and character properties without opaque XML or actions.
14#[derive(Clone, Debug, Default, Eq, PartialEq)]
15pub struct EffectiveTextProperties {
16    pub paragraph: CT_TextParagraphProperties,
17    pub run: CT_TextCharacterProperties,
18}
19
20/// The independently resolved properties for all nine DrawingML list levels.
21#[derive(Clone, Debug, Default, Eq, PartialEq)]
22pub struct EffectiveListStyle {
23    levels: [EffectiveTextProperties; 9],
24}
25
26impl EffectiveListStyle {
27    /// Returns the zero-based DrawingML level selected by `a:pPr/@lvl`.
28    pub fn level(&self, level: u8) -> Option<&EffectiveTextProperties> {
29        self.levels.get(usize::from(level))
30    }
31}
32
33impl ResolveCtx<'_> {
34    /// Resolves sources one through six for every list level of one shape.
35    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        // The colour of the style's font reference overrides the inherited
50        // text styles, and the shape's own list style overrides it in turn.
51        // A colour that does not resolve leaves the inherited colour.
52        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    /// Applies paragraph and run properties to the selected zero-based level.
70    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    /// Resolves presentation defaults and the master's other style for table text.
94    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(&paragraph),
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            &paragraph("<a:pPr marL=\"100\" marR=\"200\"><a:defRPr sz=\"1100\" b=\"1\"/></a:pPr>"),
344        );
345        merge_paragraph(
346            &mut effective,
347            &paragraph("<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, &paragraph("<a:pPr rtl=\"1\"></a:pPr>"));
360        assert_eq!(effective.paragraph.right_to_left, Some(true));
361
362        merge_paragraph(&mut effective, &paragraph("<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            &paragraph(
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            &paragraph(
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            &paragraph(
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            &paragraph("<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        // A font colour that does not resolve keeps the inherited colour.
526        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}