Skip to main content

ppt_rs/generator/text/
run.rs

1//! Text run - a span of text with consistent formatting
2
3use super::format::TextFormat;
4use super::escape_xml;
5use crate::core::ToXml;
6
7/// A run of text with consistent formatting
8#[derive(Clone, Debug)]
9pub struct Run {
10    pub text: String,
11    pub format: TextFormat,
12}
13
14impl Run {
15    /// Create a new text run
16    pub fn new(text: &str) -> Self {
17        Run {
18            text: text.to_string(),
19            format: TextFormat::default(),
20        }
21    }
22
23    /// Apply formatting
24    pub fn with_format(mut self, format: TextFormat) -> Self {
25        self.format = format;
26        self
27    }
28
29    /// Set bold
30    pub fn bold(mut self) -> Self {
31        self.format.bold = true;
32        self
33    }
34
35    /// Set italic
36    pub fn italic(mut self) -> Self {
37        self.format.italic = true;
38        self
39    }
40
41    /// Set underline
42    pub fn underline(mut self) -> Self {
43        self.format.underline = true;
44        self
45    }
46
47    /// Set color
48    pub fn color(mut self, hex: &str) -> Self {
49        self.format.color = Some(hex.trim_start_matches('#').to_uppercase());
50        self
51    }
52
53    /// Set font size
54    pub fn size(mut self, points: u32) -> Self {
55        self.format.font_size = Some(points);
56        self
57    }
58
59    /// Set font family
60    pub fn font(mut self, family: &str) -> Self {
61        self.format.font_family = Some(family.to_string());
62        self
63    }
64
65    /// Generate XML for this run
66    pub fn to_xml(&self) -> String {
67        let size = self.format.font_size.unwrap_or(18) * 100;
68        let bold = if self.format.bold { "1" } else { "0" };
69        let italic = if self.format.italic { "1" } else { "0" };
70        let underline = if self.format.underline { " u=\"sng\"" } else { "" };
71        
72        let color_xml = self.format.color.as_ref()
73            .map(|c| format!(r#"<a:solidFill><a:srgbClr val="{}"/></a:solidFill>"#, c))
74            .unwrap_or_default();
75        
76        let font_xml = self.format.font_family.as_ref()
77            .map(|f| format!(r#"<a:latin typeface="{}"/>"#, escape_xml(f)))
78            .unwrap_or_default();
79
80        format!(
81            r#"<a:r><a:rPr lang="en-US" sz="{}" b="{}" i="{}"{} dirty="0">{}{}</a:rPr><a:t>{}</a:t></a:r>"#,
82            size, bold, italic, underline, color_xml, font_xml, escape_xml(&self.text)
83        )
84    }
85}
86
87impl ToXml for Run {
88    fn to_xml(&self) -> String {
89        Run::to_xml(self)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_run_to_xml() {
99        let run = Run::new("Hello").bold().color("FF0000").size(24);
100        let xml = run.to_xml();
101        
102        assert!(xml.contains("Hello"));
103        assert!(xml.contains("b=\"1\""));
104        assert!(xml.contains("FF0000"));
105        assert!(xml.contains("sz=\"2400\""));
106    }
107
108    #[test]
109    fn test_font_family() {
110        let run = Run::new("Arial text").font("Arial");
111        let xml = run.to_xml();
112        
113        assert!(xml.contains("typeface=\"Arial\""));
114    }
115}