Skip to main content

ppt_rs/generator/layouts/
common.rs

1//! Common utilities for slide XML generation
2
3use crate::core::XmlWriter;
4use crate::generator::constants::{
5    SLIDE_WIDTH, SLIDE_HEIGHT,
6};
7use crate::generator::slide_content::BulletStyle;
8
9/// XML declaration and namespaces
10pub const XML_DECL: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#;
11pub const SLIDE_NS: &str = r#"xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main""#;
12
13/// Extended text properties for full formatting support
14#[derive(Clone, Debug, Default)]
15pub struct ExtendedTextProps {
16    pub size: u32,
17    pub bold: bool,
18    pub italic: bool,
19    pub underline: bool,
20    pub strikethrough: bool,
21    pub subscript: bool,
22    pub superscript: bool,
23    pub color: Option<String>,
24    pub highlight: Option<String>,
25    pub font_family: Option<String>,
26}
27
28impl ExtendedTextProps {
29    pub fn new(size: u32) -> Self {
30        Self {
31            size,
32            ..Default::default()
33        }
34    }
35    
36    pub fn with_basic(size: u32, bold: bool, italic: bool, underline: bool, color: Option<&str>) -> Self {
37        Self {
38            size,
39            bold,
40            italic,
41            underline,
42            color: color.map(|c| c.trim_start_matches('#').to_uppercase()),
43            ..Default::default()
44        }
45    }
46    
47    pub fn to_xml(&self) -> String {
48        let mut attrs = format!(
49            r#"<a:rPr lang="en-US" sz="{}" b="{}" i="{}" dirty="0""#,
50            self.size,
51            if self.bold { "1" } else { "0" },
52            if self.italic { "1" } else { "0" }
53        );
54
55        if self.underline {
56            attrs.push_str(r#" u="sng""#);
57        }
58        
59        if self.strikethrough {
60            attrs.push_str(r#" strike="sngStrike""#);
61        }
62        
63        if self.subscript {
64            attrs.push_str(r#" baseline="-25000""#);
65        } else if self.superscript {
66            attrs.push_str(r#" baseline="30000""#);
67        }
68
69        attrs.push('>');
70
71        if let Some(ref hex_color) = self.color {
72            let clean_color = hex_color.trim_start_matches('#').to_uppercase();
73            attrs.push_str(&format!(
74                r#"<a:solidFill><a:srgbClr val="{clean_color}"/></a:solidFill>"#
75            ));
76        }
77        
78        if let Some(ref highlight) = self.highlight {
79            let clean_color = highlight.trim_start_matches('#').to_uppercase();
80            attrs.push_str(&format!(
81                r#"<a:highlight><a:srgbClr val="{clean_color}"/></a:highlight>"#
82            ));
83        }
84        
85        if let Some(ref font) = self.font_family {
86            attrs.push_str(&format!(
87                r#"<a:latin typeface="{font}"/><a:cs typeface="{font}"/>"#
88            ));
89        }
90
91        attrs.push_str("</a:rPr>");
92        attrs
93    }
94}
95
96/// Generate text run properties XML
97pub fn generate_text_props(
98    size: u32,
99    bold: bool,
100    italic: bool,
101    underline: bool,
102    color: Option<&str>,
103) -> String {
104    ExtendedTextProps::with_basic(size, bold, italic, underline, color).to_xml()
105}
106
107/// Generate text run properties XML with extended formatting
108pub fn generate_text_props_extended(props: &ExtendedTextProps) -> String {
109    props.to_xml()
110}
111
112/// Shape position and size
113#[derive(Clone, Copy, Debug)]
114pub struct ShapePosition {
115    pub x: u32,
116    pub y: u32,
117    pub cx: u32,
118    pub cy: u32,
119}
120
121impl ShapePosition {
122    pub fn new(x: u32, y: u32, cx: u32, cy: u32) -> Self {
123        Self { x, y, cx, cy }
124    }
125}
126
127/// Text content with formatting properties
128#[derive(Clone, Debug)]
129pub struct TextContent<'a> {
130    pub text: &'a str,
131    pub props: &'a str,
132}
133
134impl<'a> TextContent<'a> {
135    pub fn new(text: &'a str, props: &'a str) -> Self {
136        Self { text, props }
137    }
138}
139
140/// Builder for slide XML with common structure
141pub struct SlideXmlBuilder {
142    writer: XmlWriter,
143}
144
145impl SlideXmlBuilder {
146    pub fn new() -> Self {
147        Self {
148            writer: XmlWriter::new(),
149        }
150    }
151
152    /// Start slide with background
153    pub fn start_slide_with_bg(mut self) -> Self {
154        self.writer.raw(XML_DECL);
155        self.writer.raw("\n<p:sld ");
156        self.writer.raw(SLIDE_NS);
157        self.writer.raw(">\n<p:cSld>\n");
158        self.writer.raw("<p:bg><p:bgRef idx=\"1001\"><a:schemeClr val=\"bg1\"/></p:bgRef></p:bg>\n");
159        self
160    }
161
162    /// Start shape tree
163    pub fn start_sp_tree(mut self) -> Self {
164        self.writer.raw("<p:spTree>\n");
165        self.writer.raw("<p:nvGrpSpPr><p:cNvPr id=\"1\" name=\"\"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>\n");
166        self.writer.raw(&format!(
167            "<p:grpSpPr><a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"{SLIDE_WIDTH}\" cy=\"{SLIDE_HEIGHT}\"/><a:chOff x=\"0\" y=\"0\"/><a:chExt cx=\"{SLIDE_WIDTH}\" cy=\"{SLIDE_HEIGHT}\"/></a:xfrm></p:grpSpPr>\n"
168        ));
169        self
170    }
171
172    /// Add title shape
173    pub fn add_title(mut self, id: u32, position: ShapePosition, content: TextContent<'_>, ph_type: &str) -> Self {
174        self.writer.raw(&format!(
175            r#"<p:sp>
176<p:nvSpPr>
177<p:cNvPr id="{}" name="Title"/>
178<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
179<p:nvPr><p:ph type="{}"/></p:nvPr>
180</p:nvSpPr>
181<p:spPr>
182<a:xfrm><a:off x="{}" y="{}"/><a:ext cx="{}" cy="{}"/></a:xfrm>
183<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
184<a:noFill/>
185</p:spPr>
186<p:txBody>
187<a:bodyPr/>
188<a:lstStyle/>
189<a:p>
190<a:r>
191{}
192<a:t>{}</a:t>
193</a:r>
194</a:p>
195</p:txBody>
196</p:sp>
197"#,
198            id, ph_type, position.x, position.y, position.cx, position.cy, content.props, escape_xml(content.text)
199        ));
200        self
201    }
202
203    /// Add centered title
204    pub fn add_centered_title(mut self, id: u32, position: ShapePosition, content: TextContent<'_>) -> Self {
205        self.writer.raw(&format!(
206            r#"<p:sp>
207<p:nvSpPr>
208<p:cNvPr id="{}" name="Title"/>
209<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
210<p:nvPr><p:ph type="ctrTitle"/></p:nvPr>
211</p:nvSpPr>
212<p:spPr>
213<a:xfrm><a:off x="{}" y="{}"/><a:ext cx="{}" cy="{}"/></a:xfrm>
214<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
215<a:noFill/>
216</p:spPr>
217<p:txBody>
218<a:bodyPr/>
219<a:lstStyle/>
220<a:p>
221<a:pPr algn="ctr"/>
222<a:r>
223{}
224<a:t>{}</a:t>
225</a:r>
226</a:p>
227</p:txBody>
228</p:sp>
229"#,
230            id, position.x, position.y, position.cx, position.cy, content.props, escape_xml(content.text)
231        ));
232        self
233    }
234
235    /// Start content body shape
236    pub fn start_content_body(mut self, id: u32, x: u32, y: u32, cx: u32, cy: u32) -> Self {
237        self.writer.raw(&format!(
238            r#"<p:sp>
239<p:nvSpPr>
240<p:cNvPr id="{}" name="Content"/>
241<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
242<p:nvPr><p:ph type="body" idx="1"/></p:nvPr>
243</p:nvSpPr>
244<p:spPr>
245<a:xfrm><a:off x="{}" y="{}"/><a:ext cx="{}" cy="{}"/></a:xfrm>
246<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
247<a:noFill/>
248</p:spPr>
249<p:txBody>
250<a:bodyPr/>
251<a:lstStyle/>
252"#,
253            id, x, y, cx, cy
254        ));
255        self
256    }
257
258    /// Add bullet paragraph
259    pub fn add_bullet(self, text: &str, props: &str, level: u32) -> Self {
260        self.add_bullet_with_style(text, props, level, BulletStyle::Bullet)
261    }
262    
263    /// Add bullet paragraph with specific style
264    pub fn add_bullet_with_style(mut self, text: &str, props: &str, level: u32, style: BulletStyle) -> Self {
265        let indent = 457200 + (level * 457200); // 0.5 inch base + 0.5 inch per level
266        let margin_left = level * 457200 + indent;
267        let bullet_xml = style.to_xml();
268        
269        self.writer.raw(&format!(
270            r#"<a:p>
271<a:pPr lvl="{}" marL="{}" indent="-{}">
272{}
273</a:pPr>
274<a:r>
275{}
276<a:t>{}</a:t>
277</a:r>
278</a:p>
279"#,
280            level, margin_left, indent, bullet_xml, props, escape_xml(text)
281        ));
282        self
283    }
284
285    /// End content body
286    pub fn end_content_body(mut self) -> Self {
287        self.writer.raw("</p:txBody>\n</p:sp>\n");
288        self
289    }
290
291    /// Add raw XML
292    pub fn raw(mut self, xml: &str) -> Self {
293        self.writer.raw(xml);
294        self
295    }
296
297    /// End shape tree
298    pub fn end_sp_tree(mut self) -> Self {
299        self.writer.raw("</p:spTree>\n");
300        self
301    }
302
303    /// End slide
304    pub fn end_slide(mut self) -> Self {
305        self.writer.raw("</p:cSld>\n<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>\n</p:sld>");
306        self
307    }
308
309    /// Build final XML string
310    pub fn build(self) -> String {
311        self.writer.finish()
312    }
313}
314
315impl Default for SlideXmlBuilder {
316    fn default() -> Self {
317        Self::new()
318    }
319}
320
321pub use crate::core::escape_xml;
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_generate_text_props() {
329        let props = generate_text_props(2400, true, false, false, Some("FF0000"));
330        assert!(props.contains("b=\"1\""));
331        assert!(props.contains("sz=\"2400\""));
332        assert!(props.contains("FF0000"));
333    }
334
335    #[test]
336    fn test_escape_xml() {
337        assert_eq!(escape_xml("a & b"), "a &amp; b");
338        assert_eq!(escape_xml("<tag>"), "&lt;tag&gt;");
339    }
340
341    #[test]
342    fn test_slide_builder() {
343        let xml = SlideXmlBuilder::new()
344            .start_slide_with_bg()
345            .start_sp_tree()
346            .end_sp_tree()
347            .end_slide()
348            .build();
349        
350        assert!(xml.contains("p:sld"));
351        assert!(xml.contains("p:spTree"));
352    }
353}