ppt_rs/generator/slide/
formatting.rs1use crate::core::escape_xml;
7
8#[derive(Debug, Clone)]
10pub struct TextSegment {
11 pub text: String,
12 pub bold: bool,
13 pub italic: bool,
14 pub code: bool,
15}
16
17pub fn parse_inline_formatting(text: &str) -> Vec<TextSegment> {
19 let mut segments = Vec::new();
20 let mut current_text = String::new();
21 let mut chars = text.chars().peekable();
22 let mut bold = false;
23 let mut italic = false;
24 let mut code = false;
25
26 while let Some(c) = chars.next() {
27 match c {
28 '`' if !code => {
29 if !current_text.is_empty() {
30 segments.push(TextSegment {
31 text: current_text.clone(),
32 bold,
33 italic,
34 code: false,
35 });
36 current_text.clear();
37 }
38 code = true;
39 }
40 '`' if code => {
41 segments.push(TextSegment {
42 text: current_text.clone(),
43 bold: false,
44 italic: false,
45 code: true,
46 });
47 current_text.clear();
48 code = false;
49 }
50 '*' | '_' if !code => {
51 if chars.peek() == Some(&c) {
52 chars.next();
53 if !current_text.is_empty() {
54 segments.push(TextSegment {
55 text: current_text.clone(),
56 bold,
57 italic,
58 code: false,
59 });
60 current_text.clear();
61 }
62 bold = !bold;
63 } else {
64 if !current_text.is_empty() {
65 segments.push(TextSegment {
66 text: current_text.clone(),
67 bold,
68 italic,
69 code: false,
70 });
71 current_text.clear();
72 }
73 italic = !italic;
74 }
75 }
76 _ => {
77 current_text.push(c);
78 }
79 }
80 }
81
82 if !current_text.is_empty() {
83 segments.push(TextSegment {
84 text: current_text,
85 bold,
86 italic,
87 code,
88 });
89 }
90
91 if segments.is_empty() {
92 segments.push(TextSegment {
93 text: text.to_string(),
94 bold: false,
95 italic: false,
96 code: false,
97 });
98 }
99
100 segments
101}
102
103pub fn generate_rich_text_runs(
105 text: &str,
106 base_size: u32,
107 base_bold: bool,
108 base_italic: bool,
109 base_color: Option<&str>,
110) -> String {
111 let segments = parse_inline_formatting(text);
112 let mut xml = String::new();
113
114 for segment in segments {
115 let size = base_size;
116 let bold = base_bold || segment.bold;
117 let italic = base_italic || segment.italic;
118 let escaped_text = escape_xml(&segment.text);
119
120 if segment.code {
121 xml.push_str(&format!(
122 r#"<a:r><a:rPr lang="en-US" sz="{}" dirty="0"><a:latin typeface="Consolas"/><a:solidFill><a:srgbClr val="C7254E"/></a:solidFill></a:rPr><a:t>{}</a:t></a:r>"#,
123 size, escaped_text
124 ));
125 } else {
126 let mut props = format!(
127 r#"<a:rPr lang="en-US" sz="{}" b="{}" i="{}" dirty="0""#,
128 size,
129 if bold { "1" } else { "0" },
130 if italic { "1" } else { "0" }
131 );
132
133 if let Some(color) = base_color {
134 props.push('>');
135 let clean_color = color.trim_start_matches('#').to_uppercase();
136 props.push_str(&format!(r#"<a:solidFill><a:srgbClr val="{}"/></a:solidFill>"#, clean_color));
137 props.push_str("</a:rPr>");
138 } else {
139 props.push_str("/>");
140 }
141
142 xml.push_str(&format!(r#"<a:r>{}<a:t>{}</a:t></a:r>"#, props, escaped_text));
143 }
144 }
145
146 xml
147}
148
149pub fn generate_text_props(
151 size: u32,
152 bold: bool,
153 italic: bool,
154 underline: bool,
155 color: Option<&str>,
156) -> String {
157 let mut props = format!(
158 r#"<a:rPr lang="en-US" sz="{}" b="{}" i="{}" dirty="0""#,
159 size,
160 if bold { "1" } else { "0" },
161 if italic { "1" } else { "0" }
162 );
163
164 if underline {
165 props.push_str(r#" u="sng""#);
166 }
167
168 props.push('>');
169
170 if let Some(hex_color) = color {
171 let clean_color = hex_color.trim_start_matches('#').to_uppercase();
172 props.push_str(&format!(
173 r#"<a:solidFill><a:srgbClr val="{clean_color}"/></a:solidFill>"#
174 ));
175 }
176
177 props.push_str("</a:rPr>");
178 props
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_parse_plain_text() {
187 let segments = parse_inline_formatting("Hello world");
188 assert_eq!(segments.len(), 1);
189 assert_eq!(segments[0].text, "Hello world");
190 assert!(!segments[0].bold);
191 assert!(!segments[0].italic);
192 }
193
194 #[test]
195 fn test_parse_bold() {
196 let segments = parse_inline_formatting("Hello **bold** world");
197 assert_eq!(segments.len(), 3);
198 assert_eq!(segments[1].text, "bold");
199 assert!(segments[1].bold);
200 }
201
202 #[test]
203 fn test_parse_italic() {
204 let segments = parse_inline_formatting("Hello *italic* world");
205 assert_eq!(segments.len(), 3);
206 assert_eq!(segments[1].text, "italic");
207 assert!(segments[1].italic);
208 }
209
210 #[test]
211 fn test_parse_code() {
212 let segments = parse_inline_formatting("Hello `code` world");
213 assert_eq!(segments.len(), 3);
214 assert_eq!(segments[1].text, "code");
215 assert!(segments[1].code);
216 }
217
218 #[test]
219 fn test_generate_rich_text() {
220 let xml = generate_rich_text_runs("Hello **bold**", 1400, false, false, None);
221 assert!(xml.contains("b=\"1\""));
222 assert!(xml.contains("Hello"));
223 assert!(xml.contains("bold"));
224 }
225
226 #[test]
227 fn test_generate_text_props() {
228 let props = generate_text_props(1400, true, false, false, Some("FF0000"));
229 assert!(props.contains("b=\"1\""));
230 assert!(props.contains("FF0000"));
231 }
232}