1use super::xmlchemy::XmlElement;
6
7#[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#[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#[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 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 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#[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#[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#[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(¶.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('&', "&")
331 .replace('<', "<")
332 .replace('>', ">")
333 .replace('"', """)
334 .replace('\'', "'")
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}