Skip to main content

rpptx_layout/
style.rs

1//! Shape-style inheritance resolution.
2
3use std::fmt;
4
5use oxml_drawing::color::ColorChoice;
6use oxml_drawing::effect::CT_EffectList;
7use oxml_drawing::fill::Fill;
8use oxml_drawing::line::CT_LineProperties;
9use oxml_drawing::shape_props::CT_ShapeProperties;
10use oxml_drawing::style_ref::FontCollectionIndex;
11use oxml_drawing::theme::CT_EffectStyle;
12use rpptx_oxml::connector::CT_ConnectionShape;
13use rpptx_oxml::shape_tree::{CT_Shape, CT_ShapeStyle};
14
15use crate::ResolveCtx;
16
17/// The modelled shape-style properties after theme lookup and direct overlays.
18#[derive(Clone, Debug, Default, Eq, PartialEq)]
19pub struct EffectiveShapeStyle {
20    pub fill: Option<Fill>,
21    pub line: Option<CT_LineProperties>,
22    pub effects: Option<CT_EffectList>,
23    pub font_collection: Option<FontCollectionIndex>,
24}
25
26/// Errors produced while resolving a shape against its theme format scheme.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub enum ResolveError {
29    StyleIndexOutOfRange {
30        reference: &'static str,
31        index: u32,
32        available: usize,
33    },
34    UnresolvedPlaceholderColor {
35        reference: &'static str,
36    },
37    ConcreteValue {
38        kind: &'static str,
39        detail: String,
40    },
41}
42
43impl fmt::Display for ResolveError {
44    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::StyleIndexOutOfRange {
47                reference,
48                index,
49                available,
50            } => write!(
51                formatter,
52                "{reference} style index {index} is outside the {available} available entries"
53            ),
54            Self::UnresolvedPlaceholderColor { reference } => write!(
55                formatter,
56                "{reference} style retains an unresolved phClr placeholder colour"
57            ),
58            Self::ConcreteValue { kind, detail } => {
59                write!(
60                    formatter,
61                    "cannot resolve {kind} to a concrete value: {detail}"
62                )
63            }
64        }
65    }
66}
67
68impl std::error::Error for ResolveError {}
69
70impl ResolveCtx<'_> {
71    /// Resolves one ordinary shape's modelled theme and direct style properties.
72    pub fn effective_shape_style(
73        &self,
74        shape: &CT_Shape,
75    ) -> Result<EffectiveShapeStyle, ResolveError> {
76        let matrix = &self.theme.theme_elements.format_scheme;
77        let referenced_effect_style = self.referenced_effect_style(shape.style())?;
78        let mut effective = if let Some(style) = shape.style() {
79            EffectiveShapeStyle {
80                fill: referenced_fill(
81                    style.fill_reference.index,
82                    &matrix.fill_styles,
83                    &matrix.background_fill_styles,
84                )?,
85                line: matrix_entry("line", style.line_reference.index, &matrix.line_styles)?,
86                effects: None,
87                font_collection: Some(style.font_reference.index),
88            }
89        } else {
90            EffectiveShapeStyle::default()
91        };
92
93        if let Some(explicit) = &shape.shape_properties.fill {
94            effective.fill = Some(explicit.clone());
95        }
96        if let Some(explicit) = &shape.shape_properties.line {
97            if let Some(line) = effective.line.as_mut() {
98                overlay_line(line, explicit);
99            } else {
100                effective.line = Some(explicit.clone());
101            }
102        }
103
104        let fill_reference = shape
105            .style()
106            .and_then(|style| style.fill_reference.color.as_ref());
107        let line_reference = shape
108            .style()
109            .and_then(|style| style.line_reference.color.as_ref());
110        if let Some(fill) = effective.fill.as_mut() {
111            substitute_fill(fill, fill_reference, "fill")?;
112        }
113        if let Some(line) = effective.line.as_mut()
114            && let Some(fill) = line.fill.as_mut()
115        {
116            substitute_fill(fill, line_reference, "line")?;
117        }
118        effective.effects = resolve_effects(
119            referenced_effect_style,
120            shape.style(),
121            &shape.shape_properties,
122        )?;
123        Ok(effective)
124    }
125
126    /// Resolves the effects of an ordinary shape or a connector: the theme
127    /// effect its style's `a:effectRef` selects, replaced as a whole by a
128    /// direct `a:effectLst`. Index 0 and an empty direct list both leave no
129    /// effect.
130    pub(crate) fn effective_effects(
131        &self,
132        style: Option<&CT_ShapeStyle>,
133        properties: &CT_ShapeProperties,
134    ) -> Result<Option<CT_EffectList>, ResolveError> {
135        let referenced_effect_style = self.referenced_effect_style(style)?;
136        resolve_effects(referenced_effect_style, style, properties)
137    }
138
139    fn referenced_effect_style(
140        &self,
141        style: Option<&CT_ShapeStyle>,
142    ) -> Result<Option<CT_EffectStyle>, ResolveError> {
143        match style {
144            Some(style) => matrix_entry(
145                "effect",
146                style.effect_reference.index,
147                &self.theme.theme_elements.format_scheme.effect_styles,
148            ),
149            None => Ok(None),
150        }
151    }
152
153    /// Resolves one connector's line from its style's theme line reference and
154    /// its direct `a:ln`, the same way an ordinary shape's line resolves.
155    pub(crate) fn effective_connector_line(
156        &self,
157        connector: &CT_ConnectionShape,
158    ) -> Result<Option<CT_LineProperties>, ResolveError> {
159        let style = connector.style();
160        let mut line = match style {
161            Some(style) => matrix_entry(
162                "line",
163                style.line_reference.index,
164                &self.theme.theme_elements.format_scheme.line_styles,
165            )?,
166            None => None,
167        };
168        if let Some(explicit) = &connector.shape_properties.line {
169            if let Some(line) = line.as_mut() {
170                overlay_line(line, explicit);
171            } else {
172                line = Some(explicit.clone());
173            }
174        }
175        if let Some(fill) = line.as_mut().and_then(|line| line.fill.as_mut()) {
176            let reference = style.and_then(|style| style.line_reference.color.as_ref());
177            substitute_fill(fill, reference, "line")?;
178        }
179        Ok(line)
180    }
181}
182
183/// Chooses the effects a shape draws from its referenced theme effect style
184/// and its direct `a:effectLst`, then resolves their placeholder colours.
185fn resolve_effects(
186    referenced_effect_style: Option<CT_EffectStyle>,
187    style: Option<&CT_ShapeStyle>,
188    properties: &CT_ShapeProperties,
189) -> Result<Option<CT_EffectList>, ResolveError> {
190    let has_explicit_unmodelled_effect = properties.has_unmodelled_effect();
191    let unmodelled_effect_has_placeholder = if has_explicit_unmodelled_effect {
192        properties.has_unmodelled_effect_placeholder_color()
193    } else {
194        referenced_effect_style
195            .as_ref()
196            .is_some_and(|style| style.has_unmodelled_effect_placeholder_color())
197    };
198    if unmodelled_effect_has_placeholder {
199        return Err(ResolveError::UnresolvedPlaceholderColor {
200            reference: "effect",
201        });
202    }
203    let mut effects = if has_explicit_unmodelled_effect {
204        None
205    } else if let Some(explicit) = &properties.effects {
206        Some(explicit.clone())
207    } else {
208        referenced_effect_style
209            .filter(|style| !style.has_unmodelled_effect())
210            .and_then(|style| style.effect_list)
211    };
212    if let Some(effects) = effects.as_mut() {
213        let effect_reference = style.and_then(|style| style.effect_reference.color.as_ref());
214        if let Some(shadow) = effects.outer_shadow.as_mut()
215            && let Some(color) = shadow.color.as_mut()
216        {
217            substitute_color(color, effect_reference, "effect")?;
218        }
219        if effects.has_unmodelled_placeholder_color() {
220            return Err(ResolveError::UnresolvedPlaceholderColor {
221                reference: "effect",
222            });
223        }
224    }
225    Ok(effects)
226}
227
228fn matrix_entry<T: Clone>(
229    reference: &'static str,
230    index: u32,
231    entries: &[T],
232) -> Result<Option<T>, ResolveError> {
233    if index == 0 {
234        return Ok(None);
235    }
236    let offset = usize::try_from(index - 1).ok();
237    offset
238        .and_then(|offset| entries.get(offset))
239        .cloned()
240        .map(Some)
241        .ok_or(ResolveError::StyleIndexOutOfRange {
242            reference,
243            index,
244            available: entries.len(),
245        })
246}
247
248pub(crate) fn referenced_fill(
249    index: u32,
250    normal: &[Fill],
251    background: &[Fill],
252) -> Result<Option<Fill>, ResolveError> {
253    if index > 1000 {
254        matrix_entry("background fill", index - 1000, background)
255    } else {
256        matrix_entry("fill", index, normal)
257    }
258}
259
260fn overlay_line(target: &mut CT_LineProperties, source: &CT_LineProperties) {
261    if source.width.is_some() {
262        target.width = source.width;
263    }
264    if source.cap.is_some() {
265        target.cap = source.cap;
266    }
267    if source.fill.is_some() {
268        target.fill.clone_from(&source.fill);
269    }
270    if source.dash.is_some() {
271        target.dash.clone_from(&source.dash);
272    }
273    if source.join.is_some() {
274        target.join.clone_from(&source.join);
275    }
276    if source.head_end.is_some() {
277        target.head_end.clone_from(&source.head_end);
278    }
279    if source.tail_end.is_some() {
280        target.tail_end.clone_from(&source.tail_end);
281    }
282}
283
284pub(crate) fn substitute_fill(
285    fill: &mut Fill,
286    reference: Option<&ColorChoice>,
287    kind: &'static str,
288) -> Result<(), ResolveError> {
289    match fill {
290        Fill::Solid(fill) => substitute_optional_color(&mut fill.color, reference, kind),
291        Fill::Gradient(fill) => {
292            for stop in &mut fill.stops {
293                substitute_optional_color(&mut stop.color, reference, kind)?;
294            }
295            Ok(())
296        }
297        Fill::Pattern(fill) => {
298            substitute_optional_color(&mut fill.foreground, reference, kind)?;
299            substitute_optional_color(&mut fill.background, reference, kind)
300        }
301        Fill::NoFill(_) | Fill::Blip(_) => Ok(()),
302    }
303}
304
305fn substitute_optional_color(
306    color: &mut Option<ColorChoice>,
307    reference: Option<&ColorChoice>,
308    kind: &'static str,
309) -> Result<(), ResolveError> {
310    if let Some(color) = color {
311        substitute_color(color, reference, kind)?;
312    }
313    Ok(())
314}
315
316fn substitute_color(
317    color: &mut ColorChoice,
318    reference: Option<&ColorChoice>,
319    kind: &'static str,
320) -> Result<(), ResolveError> {
321    if !matches!(color, ColorChoice::Scheme { value, .. } if value == "phClr") {
322        return Ok(());
323    }
324    let placeholder_transforms = color.transforms().to_vec();
325    let Some(mut replacement) = reference.cloned() else {
326        return Err(ResolveError::UnresolvedPlaceholderColor { reference: kind });
327    };
328    if matches!(&replacement, ColorChoice::Scheme { value, .. } if value == "phClr") {
329        return Err(ResolveError::UnresolvedPlaceholderColor { reference: kind });
330    }
331    transforms_mut(&mut replacement).extend(placeholder_transforms);
332    *color = replacement;
333    Ok(())
334}
335
336fn transforms_mut(color: &mut ColorChoice) -> &mut Vec<oxml_drawing::color::ColorTransform> {
337    match color {
338        ColorChoice::Srgb { transforms, .. }
339        | ColorChoice::Scheme { transforms, .. }
340        | ColorChoice::System { transforms, .. }
341        | ColorChoice::Preset { transforms, .. } => transforms,
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use oxml_drawing::color::{ColorChoice, ColorMap, ColorTransform};
348    use oxml_drawing::fill::Fill;
349    use oxml_drawing::line::{LineCap, LineDash, ST_PresetLineDashVal};
350    use oxml_drawing::style_ref::FontCollectionIndex;
351    use oxml_drawing::text::CT_TextListStyle;
352    use oxml_drawing::theme::CT_OfficeStyleSheet;
353    use rpptx_oxml::shape_tree::{CT_Shape, ShapeTreeChild};
354    use rpptx_oxml::slide_parts::{CT_Slide, CT_SlideLayout, CT_SlideMaster};
355
356    use super::{ResolveCtx, ResolveError};
357
358    const P_NS: &str = "http://schemas.openxmlformats.org/presentationml/2006/main";
359    const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
360
361    #[test]
362    fn shape_with_style_resolves_theme_fill_with_reference_colour_substituted() {
363        let fixture = Fixture::new(&shape("", &style_refs(0, 1, 0, "minor", "AABBCC")));
364        let resolved = fixture
365            .context()
366            .effective_shape_style(fixture.shape(0))
367            .unwrap();
368
369        let Some(Fill::Solid(fill)) = resolved.fill else {
370            panic!("expected a resolved solid fill");
371        };
372        let Some(ColorChoice::Srgb {
373            value, transforms, ..
374        }) = fill.color
375        else {
376            panic!("expected the reference sRGB colour");
377        };
378        assert_eq!(value.to_string(), "AABBCC");
379        assert!(transforms.is_empty());
380        assert_eq!(resolved.font_collection, Some(FontCollectionIndex::Minor));
381    }
382
383    #[test]
384    fn fill_ref_1001_selects_first_background_fill() {
385        let fixture = Fixture::new(&shape("", &style_refs(0, 1001, 0, "major", "102030")));
386        let resolved = fixture
387            .context()
388            .effective_shape_style(fixture.shape(0))
389            .unwrap();
390
391        let Some(Fill::Solid(fill)) = resolved.fill else {
392            panic!("expected the first background solid fill");
393        };
394        assert!(matches!(
395            fill.color,
396            Some(ColorChoice::Srgb { value, .. }) if value.to_string() == "102030"
397        ));
398    }
399
400    #[test]
401    fn style_matrix_zero_is_none_and_out_of_range_is_error() {
402        let shapes = [
403            shape("", &style_refs(0, 0, 0, "none", "112233")),
404            shape("", &style_refs(0, 4, 0, "none", "112233")),
405        ]
406        .join("");
407        let fixture = Fixture::new(&shapes);
408
409        let zero = fixture
410            .context()
411            .effective_shape_style(fixture.shape(0))
412            .unwrap();
413        assert!(zero.fill.is_none());
414        assert!(zero.line.is_none());
415        assert!(zero.effects.is_none());
416        assert_eq!(zero.font_collection, Some(FontCollectionIndex::None));
417
418        assert_eq!(
419            fixture
420                .context()
421                .effective_shape_style(fixture.shape(1))
422                .unwrap_err(),
423            ResolveError::StyleIndexOutOfRange {
424                reference: "fill",
425                index: 4,
426                available: 3,
427            }
428        );
429    }
430
431    #[test]
432    fn placeholder_colour_substitution_keeps_transform_order() {
433        let mut fixture = Fixture::new(&shape(
434            "",
435            &style_refs_with_fill_colour(
436                1,
437                r#"<a:schemeClr val="accent2"><a:shade val="80000"/></a:schemeClr>"#,
438            ),
439        ));
440        fixture.theme.theme_elements.format_scheme.fill_styles[0] = Fill::from_xml(
441            br#"<a:solidFill><a:schemeClr val="phClr"><a:tint val="30000"/></a:schemeClr></a:solidFill>"#,
442        )
443        .unwrap();
444
445        let resolved = fixture
446            .context()
447            .effective_shape_style(fixture.shape(0))
448            .unwrap();
449        let Some(Fill::Solid(fill)) = resolved.fill else {
450            panic!("expected a solid fill");
451        };
452        let Some(ColorChoice::Scheme {
453            value, transforms, ..
454        }) = fill.color
455        else {
456            panic!("expected a substituted scheme colour");
457        };
458        assert_eq!(value, "accent2");
459        assert!(matches!(
460            transforms.as_slice(),
461            [ColorTransform::Shade(shade), ColorTransform::Tint(tint)]
462                if shade.0 == 80_000 && tint.0 == 30_000
463        ));
464    }
465
466    #[test]
467    fn explicit_shape_properties_overlay_the_theme_style() {
468        let fixture = Fixture::new(&shape(
469            r#"<a:noFill/><a:ln w="25400"><a:prstDash val="dot"/></a:ln><a:effectLst/>"#,
470            &style_refs(2, 1, 3, "minor", "445566"),
471        ));
472        let resolved = fixture
473            .context()
474            .effective_shape_style(fixture.shape(0))
475            .unwrap();
476
477        assert!(matches!(resolved.fill, Some(Fill::NoFill(_))));
478        let line = resolved.line.unwrap();
479        assert_eq!(line.width, Some(25_400));
480        assert_eq!(line.cap, Some(LineCap::Flat));
481        assert!(matches!(
482            line.dash,
483            Some(LineDash::Preset(ref dash)) if dash.value == ST_PresetLineDashVal::Dot
484        ));
485        assert!(matches!(
486            line.fill,
487            Some(Fill::Solid(fill))
488                if matches!(fill.color, Some(ColorChoice::Srgb { value, .. }) if value.to_string() == "445566")
489        ));
490        assert!(resolved.effects.unwrap().outer_shadow.is_none());
491        assert_eq!(resolved.font_collection, Some(FontCollectionIndex::Minor));
492    }
493
494    #[test]
495    fn modelled_effect_placeholder_colour_is_substituted() {
496        let mut fixture = Fixture::new(&shape("", &style_refs(0, 0, 3, "none", "667788")));
497        let Fill::Solid(fill) = Fill::from_xml(
498            br#"<a:solidFill><a:schemeClr val="phClr"><a:tint val="25000"/></a:schemeClr></a:solidFill>"#,
499        )
500        .unwrap()
501        else {
502            unreachable!();
503        };
504        fixture.theme.theme_elements.format_scheme.effect_styles[2]
505            .effect_list
506            .as_mut()
507            .unwrap()
508            .outer_shadow
509            .as_mut()
510            .unwrap()
511            .color = fill.color;
512
513        let resolved = fixture
514            .context()
515            .effective_shape_style(fixture.shape(0))
516            .unwrap();
517        let color = resolved
518            .effects
519            .unwrap()
520            .outer_shadow
521            .unwrap()
522            .color
523            .unwrap();
524        assert!(matches!(
525            color,
526            ColorChoice::Srgb { value, transforms, .. }
527                if value.to_string() == "667788"
528                    && matches!(transforms.as_slice(), [ColorTransform::Tint(tint)] if tint.0 == 25_000)
529        ));
530    }
531
532    #[test]
533    fn unmodelled_effect_with_placeholder_colour_is_rejected() {
534        let fixture = Fixture::new(&shape(
535            r#"<a:effectLst><a:glow rad="100"><a:schemeClr val="phClr"/></a:glow></a:effectLst>"#,
536            &style_refs(0, 0, 0, "none", "778899"),
537        ));
538
539        assert_eq!(
540            fixture
541                .context()
542                .effective_shape_style(fixture.shape(0))
543                .unwrap_err(),
544            ResolveError::UnresolvedPlaceholderColor {
545                reference: "effect",
546            }
547        );
548    }
549
550    #[test]
551    fn explicit_opaque_effect_dag_replaces_theme_effect() {
552        let fixture = Fixture::new(&shape(
553            r#"<a:effectDag><a:cont/></a:effectDag>"#,
554            &style_refs(0, 0, 3, "none", "778899"),
555        ));
556
557        let resolved = fixture
558            .context()
559            .effective_shape_style(fixture.shape(0))
560            .unwrap();
561        assert!(resolved.effects.is_none());
562    }
563
564    #[test]
565    fn explicit_opaque_effect_dag_with_placeholder_colour_is_rejected() {
566        let fixture = Fixture::new(&shape(
567            r#"<a:effectDag><a:cont><a:effectLst><a:glow rad="100"><a:schemeClr val="phClr"/></a:glow></a:effectLst></a:cont></a:effectDag>"#,
568            &style_refs(0, 0, 3, "none", "778899"),
569        ));
570
571        assert_eq!(
572            fixture
573                .context()
574                .effective_shape_style(fixture.shape(0))
575                .unwrap_err(),
576            ResolveError::UnresolvedPlaceholderColor {
577                reference: "effect",
578            }
579        );
580    }
581
582    #[test]
583    fn theme_opaque_effect_dag_with_placeholder_colour_is_rejected() {
584        let mut fixture = Fixture::new(&shape("", &style_refs(0, 0, 1, "none", "778899")));
585        let xml = String::from_utf8(fixture.theme.to_xml().unwrap()).unwrap();
586        let xml = xml.replacen(
587            "<a:effectLst/>",
588            r#"<a:effectDag><a:cont><a:effectLst><a:glow rad="100"><a:schemeClr val="phClr"/></a:glow></a:effectLst></a:cont></a:effectDag>"#,
589            1,
590        );
591        fixture.theme = CT_OfficeStyleSheet::from_xml(xml.as_bytes()).unwrap();
592
593        assert_eq!(
594            fixture
595                .context()
596                .effective_shape_style(fixture.shape(0))
597                .unwrap_err(),
598            ResolveError::UnresolvedPlaceholderColor {
599                reference: "effect",
600            }
601        );
602    }
603
604    #[test]
605    fn foreign_namespace_effect_dag_does_not_replace_theme_effect() {
606        let fixture = Fixture::new(&shape(
607            r#"<x:effectDag xmlns:x="urn:extension"><x:schemeClr val="phClr"/></x:effectDag>"#,
608            &style_refs(0, 0, 3, "none", "778899"),
609        ));
610
611        let resolved = fixture
612            .context()
613            .effective_shape_style(fixture.shape(0))
614            .unwrap();
615        assert!(resolved.effects.is_some());
616    }
617
618    #[test]
619    fn foreign_namespace_scheme_color_is_not_a_placeholder_color() {
620        let fixture = Fixture::new(&shape(
621            r#"<a:effectDag><a:cont><x:schemeClr xmlns:x="urn:extension" val="phClr"/></a:cont></a:effectDag>"#,
622            &style_refs(0, 0, 3, "none", "778899"),
623        ));
624
625        let resolved = fixture
626            .context()
627            .effective_shape_style(fixture.shape(0))
628            .unwrap();
629        assert!(resolved.effects.is_none());
630    }
631
632    struct Fixture {
633        theme: CT_OfficeStyleSheet,
634        master: CT_SlideMaster,
635        layout: CT_SlideLayout,
636        slide: CT_Slide,
637        default_text_style: CT_TextListStyle,
638    }
639
640    impl Fixture {
641        fn new(shapes: &str) -> Self {
642            Self {
643                theme: CT_OfficeStyleSheet::office_default(),
644                master: CT_SlideMaster::from_xml(master_xml().as_bytes()).unwrap(),
645                layout: CT_SlideLayout::from_xml(layout_xml().as_bytes()).unwrap(),
646                slide: CT_Slide::from_xml(slide_xml(shapes).as_bytes()).unwrap(),
647                default_text_style: CT_TextListStyle::default(),
648            }
649        }
650
651        fn context(&self) -> ResolveCtx<'_> {
652            ResolveCtx::new(
653                &self.theme,
654                ColorMap::default(),
655                &self.master,
656                &self.layout,
657                &self.slide,
658                &self.default_text_style,
659            )
660        }
661
662        fn shape(&self, index: usize) -> &CT_Shape {
663            let ShapeTreeChild::Shape(shape) =
664                &self.slide.common_slide_data.shape_tree.children[index]
665            else {
666                panic!("expected an ordinary shape");
667            };
668            shape
669        }
670    }
671
672    fn style_refs(line: u32, fill: u32, effect: u32, font: &str, color: &str) -> String {
673        format!(
674            r#"<p:style><a:lnRef idx="{line}"><a:srgbClr val="{color}"/></a:lnRef><a:fillRef idx="{fill}"><a:srgbClr val="{color}"/></a:fillRef><a:effectRef idx="{effect}"><a:srgbClr val="{color}"/></a:effectRef><a:fontRef idx="{font}"><a:srgbClr val="{color}"/></a:fontRef></p:style>"#
675        )
676    }
677
678    fn style_refs_with_fill_colour(fill: u32, fill_colour: &str) -> String {
679        format!(
680            r#"<p:style><a:lnRef idx="0"><a:srgbClr val="000000"/></a:lnRef><a:fillRef idx="{fill}">{fill_colour}</a:fillRef><a:effectRef idx="0"><a:srgbClr val="000000"/></a:effectRef><a:fontRef idx="minor"><a:srgbClr val="000000"/></a:fontRef></p:style>"#
681        )
682    }
683
684    fn shape(properties: &str, style: &str) -> String {
685        format!(
686            "<p:sp><p:nvSpPr><p:cNvPr/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr>{properties}</p:spPr>{style}</p:sp>"
687        )
688    }
689
690    fn slide_xml(shapes: &str) -> String {
691        format!(
692            "<p:sld xmlns:p=\"{P_NS}\" xmlns:a=\"{A_NS}\"><p:cSld>{}</p:cSld></p:sld>",
693            shape_tree(shapes)
694        )
695    }
696
697    fn layout_xml() -> String {
698        format!(
699            "<p:sldLayout xmlns:p=\"{P_NS}\" xmlns:a=\"{A_NS}\"><p:cSld>{}</p:cSld></p:sldLayout>",
700            shape_tree("")
701        )
702    }
703
704    fn master_xml() -> String {
705        format!(
706            "<p:sldMaster xmlns:p=\"{P_NS}\" 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:sldMaster>",
707            shape_tree("")
708        )
709    }
710
711    fn shape_tree(shapes: &str) -> String {
712        format!("<p:spTree><p:nvGrpSpPr/><p:grpSpPr/>{shapes}</p:spTree>")
713    }
714}