1use sheetkit_xml::shared_strings::{BoolVal, Color, FontName, FontSize, RPr, Si, R, T};
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct RichTextRun {
12 pub text: String,
14 pub font: Option<String>,
16 pub size: Option<f64>,
18 pub bold: bool,
20 pub italic: bool,
22 pub color: Option<String>,
24}
25
26pub fn run_to_xml(run: &RichTextRun) -> R {
28 let has_formatting =
29 run.bold || run.italic || run.font.is_some() || run.size.is_some() || run.color.is_some();
30
31 let r_pr = if has_formatting {
32 Some(RPr {
33 b: if run.bold {
34 Some(BoolVal { val: None })
35 } else {
36 None
37 },
38 i: if run.italic {
39 Some(BoolVal { val: None })
40 } else {
41 None
42 },
43 sz: run.size.map(|val| FontSize { val }),
44 color: run.color.as_ref().map(|rgb| Color {
45 rgb: Some(rgb.clone()),
46 theme: None,
47 tint: None,
48 }),
49 r_font: run.font.as_ref().map(|val| FontName { val: val.clone() }),
50 family: None,
51 scheme: None,
52 })
53 } else {
54 None
55 };
56
57 R {
58 r_pr,
59 t: T {
60 xml_space: if run.text.starts_with(' ')
61 || run.text.ends_with(' ')
62 || run.text.contains(" ")
63 || run.text.contains('\n')
64 || run.text.contains('\t')
65 {
66 Some("preserve".to_string())
67 } else {
68 None
69 },
70 value: run.text.clone(),
71 },
72 }
73}
74
75pub fn xml_to_run(r: &R) -> RichTextRun {
77 let (font, size, bold, italic, color) = if let Some(ref rpr) = r.r_pr {
78 (
79 rpr.r_font.as_ref().map(|f| f.val.clone()),
80 rpr.sz.as_ref().map(|s| s.val),
81 rpr.b.is_some(),
82 rpr.i.is_some(),
83 rpr.color.as_ref().and_then(|c| c.rgb.clone()),
84 )
85 } else {
86 (None, None, false, false, None)
87 };
88
89 RichTextRun {
90 text: r.t.value.clone(),
91 font,
92 size,
93 bold,
94 italic,
95 color,
96 }
97}
98
99pub fn runs_to_si(runs: &[RichTextRun]) -> Si {
101 Si {
102 t: None,
103 r: runs.iter().map(run_to_xml).collect(),
104 }
105}
106
107pub fn rich_text_to_plain(runs: &[RichTextRun]) -> String {
109 runs.iter().map(|r| r.text.as_str()).collect()
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_rich_text_to_plain() {
118 let runs = vec![
119 RichTextRun {
120 text: "Hello ".to_string(),
121 font: None,
122 size: None,
123 bold: true,
124 italic: false,
125 color: None,
126 },
127 RichTextRun {
128 text: "World".to_string(),
129 font: None,
130 size: None,
131 bold: false,
132 italic: false,
133 color: None,
134 },
135 ];
136 assert_eq!(rich_text_to_plain(&runs), "Hello World");
137 }
138
139 #[test]
140 fn test_run_to_xml_plain() {
141 let run = RichTextRun {
142 text: "plain".to_string(),
143 font: None,
144 size: None,
145 bold: false,
146 italic: false,
147 color: None,
148 };
149 let xml_r = run_to_xml(&run);
150 assert!(xml_r.r_pr.is_none());
151 assert_eq!(xml_r.t.value, "plain");
152 }
153
154 #[test]
155 fn test_run_to_xml_bold() {
156 let run = RichTextRun {
157 text: "bold".to_string(),
158 font: None,
159 size: None,
160 bold: true,
161 italic: false,
162 color: None,
163 };
164 let xml_r = run_to_xml(&run);
165 assert!(xml_r.r_pr.is_some());
166 assert!(xml_r.r_pr.as_ref().unwrap().b.is_some());
167 }
168
169 #[test]
170 fn test_xml_to_run_roundtrip() {
171 let original = RichTextRun {
172 text: "test".to_string(),
173 font: Some("Arial".to_string()),
174 size: Some(12.0),
175 bold: true,
176 italic: true,
177 color: Some("#FF0000".to_string()),
178 };
179 let xml_r = run_to_xml(&original);
180 let back = xml_to_run(&xml_r);
181 assert_eq!(original, back);
182 }
183
184 #[test]
185 fn test_runs_to_si() {
186 let runs = vec![
187 RichTextRun {
188 text: "A".to_string(),
189 font: None,
190 size: None,
191 bold: true,
192 italic: false,
193 color: None,
194 },
195 RichTextRun {
196 text: "B".to_string(),
197 font: None,
198 size: None,
199 bold: false,
200 italic: false,
201 color: None,
202 },
203 ];
204 let si = runs_to_si(&runs);
205 assert!(si.t.is_none());
206 assert_eq!(si.r.len(), 2);
207 }
208
209 #[test]
210 fn test_xml_to_run_no_formatting() {
211 let r = R {
212 r_pr: None,
213 t: T {
214 xml_space: None,
215 value: "text".to_string(),
216 },
217 };
218 let run = xml_to_run(&r);
219 assert_eq!(run.text, "text");
220 assert!(!run.bold);
221 assert!(!run.italic);
222 assert!(run.font.is_none());
223 assert!(run.size.is_none());
224 assert!(run.color.is_none());
225 }
226
227 #[test]
228 fn test_space_preservation() {
229 let run = RichTextRun {
230 text: " leading space".to_string(),
231 font: None,
232 size: None,
233 bold: false,
234 italic: false,
235 color: None,
236 };
237 let xml_r = run_to_xml(&run);
238 assert_eq!(xml_r.t.xml_space, Some("preserve".to_string()));
239 }
240}