Skip to main content

ppt_rs/oxml/
text.rs

1//! Text XML elements for OOXML
2//!
3//! Provides types for parsing and generating DrawingML text elements.
4
5use super::xmlchemy::XmlElement;
6
7/// Text body properties (a:bodyPr)
8#[derive(Debug, Clone, Default)]
9pub struct BodyProperties {
10    pub wrap: Option<String>,
11    pub anchor: Option<String>,
12    pub anchor_ctr: bool,
13    pub rtl_col: bool,
14    pub left_inset: Option<u32>,
15    pub right_inset: Option<u32>,
16    pub top_inset: Option<u32>,
17    pub bottom_inset: Option<u32>,
18}
19
20impl BodyProperties {
21    pub fn parse(elem: &XmlElement) -> Self {
22        BodyProperties {
23            wrap: elem.attr("wrap").map(|s| s.to_string()),
24            anchor: elem.attr("anchor").map(|s| s.to_string()),
25            anchor_ctr: elem.attr("anchorCtr").map(|v| v == "1").unwrap_or(false),
26            rtl_col: elem.attr("rtlCol").map(|v| v == "1").unwrap_or(false),
27            left_inset: elem.attr("lIns").and_then(|v| v.parse().ok()),
28            right_inset: elem.attr("rIns").and_then(|v| v.parse().ok()),
29            top_inset: elem.attr("tIns").and_then(|v| v.parse().ok()),
30            bottom_inset: elem.attr("bIns").and_then(|v| v.parse().ok()),
31        }
32    }
33
34    pub fn to_xml(&self) -> String {
35        let mut attrs = Vec::new();
36        
37        if let Some(ref wrap) = self.wrap {
38            attrs.push(format!(r#"wrap="{wrap}""#));
39        }
40        if let Some(ref anchor) = self.anchor {
41            attrs.push(format!(r#"anchor="{anchor}""#));
42        }
43        if self.rtl_col {
44            attrs.push(r#"rtlCol="1""#.to_string());
45        }
46        if let Some(l) = self.left_inset {
47            attrs.push(format!(r#"lIns="{l}""#));
48        }
49        if let Some(r) = self.right_inset {
50            attrs.push(format!(r#"rIns="{r}""#));
51        }
52        if let Some(t) = self.top_inset {
53            attrs.push(format!(r#"tIns="{t}""#));
54        }
55        if let Some(b) = self.bottom_inset {
56            attrs.push(format!(r#"bIns="{b}""#));
57        }
58
59        if attrs.is_empty() {
60            "<a:bodyPr/>".to_string()
61        } else {
62            format!("<a:bodyPr {}/>", attrs.join(" "))
63        }
64    }
65}
66
67/// Paragraph properties (a:pPr)
68#[derive(Debug, Clone, Default)]
69pub struct ParagraphProperties {
70    pub align: Option<String>,
71    pub level: u32,
72    pub indent: Option<i32>,
73    pub margin_left: Option<i32>,
74    pub rtl: bool,
75}
76
77impl ParagraphProperties {
78    pub fn parse(elem: &XmlElement) -> Self {
79        ParagraphProperties {
80            align: elem.attr("algn").map(|s| s.to_string()),
81            level: elem.attr("lvl").and_then(|v| v.parse().ok()).unwrap_or(0),
82            indent: elem.attr("indent").and_then(|v| v.parse().ok()),
83            margin_left: elem.attr("marL").and_then(|v| v.parse().ok()),
84            rtl: elem.attr("rtl").map(|v| v == "1").unwrap_or(false),
85        }
86    }
87
88    pub fn to_xml(&self) -> String {
89        let mut attrs = Vec::new();
90        
91        if let Some(ref align) = self.align {
92            attrs.push(format!(r#"algn="{align}""#));
93        }
94        if self.level > 0 {
95            let level = self.level;
96            attrs.push(format!(r#"lvl="{level}""#));
97        }
98        if let Some(indent) = self.indent {
99            attrs.push(format!(r#"indent="{indent}""#));
100        }
101        if let Some(mar_l) = self.margin_left {
102            attrs.push(format!(r#"marL="{mar_l}""#));
103        }
104        if self.rtl {
105            attrs.push(r#"rtl="1""#.to_string());
106        }
107
108        if attrs.is_empty() {
109            "<a:pPr/>".to_string()
110        } else {
111            format!("<a:pPr {}/>", attrs.join(" "))
112        }
113    }
114}
115
116/// Run properties (a:rPr)
117#[derive(Debug, Clone, Default)]
118pub struct RunProperties {
119    pub lang: Option<String>,
120    pub size: Option<u32>,
121    pub bold: bool,
122    pub italic: bool,
123    pub underline: Option<String>,
124    pub strike: Option<String>,
125    pub color: Option<String>,
126    pub font_family: Option<String>,
127}
128
129impl RunProperties {
130    pub fn parse(elem: &XmlElement) -> Self {
131        let mut props = RunProperties {
132            lang: elem.attr("lang").map(|s| s.to_string()),
133            size: elem.attr("sz").and_then(|v| v.parse().ok()),
134            bold: elem.attr("b").map(|v| v == "1").unwrap_or(false),
135            italic: elem.attr("i").map(|v| v == "1").unwrap_or(false),
136            underline: elem.attr("u").map(|s| s.to_string()),
137            strike: elem.attr("strike").map(|s| s.to_string()),
138            color: None,
139            font_family: None,
140        };
141
142        // Parse color from solidFill
143        if let Some(solid_fill) = elem.find_descendant("solidFill") {
144            if let Some(srgb) = solid_fill.find("srgbClr") {
145                props.color = srgb.attr("val").map(|s| s.to_string());
146            }
147        }
148
149        // Parse font family from latin
150        if let Some(latin) = elem.find("latin") {
151            props.font_family = latin.attr("typeface").map(|s| s.to_string());
152        }
153
154        props
155    }
156
157    pub fn to_xml(&self) -> String {
158        let mut attrs = vec![r#"lang="en-US""#.to_string()];
159        
160        if let Some(sz) = self.size {
161            attrs.push(format!(r#"sz="{sz}""#));
162        }
163        let b = if self.bold { "1" } else { "0" };
164        let i = if self.italic { "1" } else { "0" };
165        attrs.push(format!(r#"b="{b}""#));
166        attrs.push(format!(r#"i="{i}""#));
167        
168        if let Some(ref u) = self.underline {
169            attrs.push(format!(r#"u="{u}""#));
170        }
171        if let Some(ref strike) = self.strike {
172            attrs.push(format!(r#"strike="{strike}""#));
173        }
174
175        let mut inner = String::new();
176        if let Some(ref color) = self.color {
177            inner.push_str(&format!(r#"<a:solidFill><a:srgbClr val="{color}"/></a:solidFill>"#));
178        }
179        if let Some(ref font) = self.font_family {
180            inner.push_str(&format!(r#"<a:latin typeface="{font}"/>"#));
181        }
182
183        if inner.is_empty() {
184            format!("<a:rPr {}/>", attrs.join(" "))
185        } else {
186            format!("<a:rPr {}>{}</a:rPr>", attrs.join(" "), inner)
187        }
188    }
189}
190
191/// Text run (a:r)
192#[derive(Debug, Clone)]
193pub struct TextRun {
194    pub text: String,
195    pub properties: RunProperties,
196}
197
198impl TextRun {
199    pub fn new(text: &str) -> Self {
200        TextRun {
201            text: text.to_string(),
202            properties: RunProperties::default(),
203        }
204    }
205
206    pub fn parse(elem: &XmlElement) -> Option<Self> {
207        let text = elem.find("t").map(|t| t.text_content())?;
208        let properties = elem.find("rPr")
209            .map(|rpr| RunProperties::parse(rpr))
210            .unwrap_or_default();
211
212        Some(TextRun { text, properties })
213    }
214
215    pub fn to_xml(&self) -> String {
216        format!(
217            "<a:r>{}<a:t>{}</a:t></a:r>",
218            self.properties.to_xml(),
219            escape_xml(&self.text)
220        )
221    }
222}
223
224/// Paragraph (a:p)
225#[derive(Debug, Clone)]
226pub struct TextParagraph {
227    pub properties: ParagraphProperties,
228    pub runs: Vec<TextRun>,
229}
230
231impl TextParagraph {
232    pub fn new() -> Self {
233        TextParagraph {
234            properties: ParagraphProperties::default(),
235            runs: Vec::new(),
236        }
237    }
238
239    pub fn parse(elem: &XmlElement) -> Self {
240        let properties = elem.find("pPr")
241            .map(|ppr| ParagraphProperties::parse(ppr))
242            .unwrap_or_default();
243
244        let runs = elem.find_all("r")
245            .into_iter()
246            .filter_map(|r| TextRun::parse(r))
247            .collect();
248
249        TextParagraph { properties, runs }
250    }
251
252    pub fn to_xml(&self) -> String {
253        let mut xml = String::from("<a:p>");
254        xml.push_str(&self.properties.to_xml());
255        for run in &self.runs {
256            xml.push_str(&run.to_xml());
257        }
258        xml.push_str("</a:p>");
259        xml
260    }
261
262    pub fn text(&self) -> String {
263        self.runs.iter().map(|r| r.text.as_str()).collect()
264    }
265}
266
267impl Default for TextParagraph {
268    fn default() -> Self {
269        Self::new()
270    }
271}
272
273/// Text body (p:txBody or a:txBody)
274#[derive(Debug, Clone)]
275pub struct TextBody {
276    pub body_properties: BodyProperties,
277    pub paragraphs: Vec<TextParagraph>,
278}
279
280impl TextBody {
281    pub fn new() -> Self {
282        TextBody {
283            body_properties: BodyProperties::default(),
284            paragraphs: Vec::new(),
285        }
286    }
287
288    pub fn parse(elem: &XmlElement) -> Self {
289        let body_properties = elem.find("bodyPr")
290            .map(|bp| BodyProperties::parse(bp))
291            .unwrap_or_default();
292
293        let paragraphs = elem.find_all("p")
294            .into_iter()
295            .map(|p| TextParagraph::parse(p))
296            .collect();
297
298        TextBody { body_properties, paragraphs }
299    }
300
301    pub fn to_xml(&self) -> String {
302        let mut xml = String::from("<p:txBody>");
303        xml.push_str(&self.body_properties.to_xml());
304        xml.push_str("<a:lstStyle/>");
305        for para in &self.paragraphs {
306            xml.push_str(&para.to_xml());
307        }
308        if self.paragraphs.is_empty() {
309            xml.push_str("<a:p/>");
310        }
311        xml.push_str("</p:txBody>");
312        xml
313    }
314
315    pub fn all_text(&self) -> String {
316        self.paragraphs.iter()
317            .map(|p| p.text())
318            .collect::<Vec<_>>()
319            .join("\n")
320    }
321}
322
323impl Default for TextBody {
324    fn default() -> Self {
325        Self::new()
326    }
327}
328
329fn escape_xml(s: &str) -> String {
330    s.replace('&', "&amp;")
331        .replace('<', "&lt;")
332        .replace('>', "&gt;")
333        .replace('"', "&quot;")
334        .replace('\'', "&apos;")
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_run_properties_to_xml() {
343        let mut props = RunProperties::default();
344        props.bold = true;
345        props.size = Some(2400);
346        props.color = Some("FF0000".to_string());
347
348        let xml = props.to_xml();
349        assert!(xml.contains("b=\"1\""));
350        assert!(xml.contains("sz=\"2400\""));
351        assert!(xml.contains("FF0000"));
352    }
353
354    #[test]
355    fn test_text_run_to_xml() {
356        let run = TextRun::new("Hello World");
357        let xml = run.to_xml();
358        
359        assert!(xml.contains("<a:r>"));
360        assert!(xml.contains("Hello World"));
361        assert!(xml.contains("</a:r>"));
362    }
363
364    #[test]
365    fn test_paragraph_to_xml() {
366        let mut para = TextParagraph::new();
367        para.runs.push(TextRun::new("Test"));
368        
369        let xml = para.to_xml();
370        assert!(xml.contains("<a:p>"));
371        assert!(xml.contains("Test"));
372    }
373
374    #[test]
375    fn test_text_body_to_xml() {
376        let mut body = TextBody::new();
377        let mut para = TextParagraph::new();
378        para.runs.push(TextRun::new("Content"));
379        body.paragraphs.push(para);
380
381        let xml = body.to_xml();
382        assert!(xml.contains("<p:txBody>"));
383        assert!(xml.contains("Content"));
384    }
385}