1mod xml;
2
3pub use xml::*;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, PartialEq)]
7pub struct Font {
8 pub name: String,
9 pub size: f64,
10 pub bold: bool,
11 pub italic: bool,
12 pub underline: Option<UnderlineStyle>,
13 pub color: Option<Color>,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17pub enum UnderlineStyle {
18 Single,
19 Double,
20 SingleAccounting,
21 DoubleAccounting,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub struct Color {
26 pub rgb: String, }
28
29impl Color {
30 pub fn new(rgb: &str) -> Self {
31 Self {
32 rgb: rgb.to_string(),
33 }
34 }
35
36 pub fn from_rgb(r: u8, g: u8, b: u8) -> Self {
37 Self {
38 rgb: format!("FF{:02X}{:02X}{:02X}", r, g, b),
39 }
40 }
41
42 pub fn from_argb(a: u8, r: u8, g: u8, b: u8) -> Self {
43 Self {
44 rgb: format!("{:02X}{:02X}{:02X}{:02X}", a, r, g, b),
45 }
46 }
47
48 pub fn is_valid(&self) -> bool {
49 self.rgb.len() == 8 && self.rgb.chars().all(|c| c.is_ascii_hexdigit())
50 }
51
52 pub const BLACK: &'static str = "FF000000";
54 pub const WHITE: &'static str = "FFFFFFFF";
55 pub const RED: &'static str = "FFFF0000";
56 pub const GREEN: &'static str = "FF00FF00";
57 pub const BLUE: &'static str = "FF0000FF";
58 pub const GRAY: &'static str = "FF808080";
59
60 pub fn black() -> Self {
62 Self::new(Self::BLACK)
63 }
64
65 pub fn white() -> Self {
66 Self::new(Self::WHITE)
67 }
68
69 pub fn red() -> Self {
70 Self::new(Self::RED)
71 }
72
73 pub fn green() -> Self {
74 Self::new(Self::GREEN)
75 }
76
77 pub fn blue() -> Self {
78 Self::new(Self::BLUE)
79 }
80
81 pub fn gray() -> Self {
82 Self::new(Self::GRAY)
83 }
84
85 pub fn get_components(&self) -> Option<(u8, u8, u8, u8)> {
87 if !self.is_valid() {
88 return None;
89 }
90
91 let a = u8::from_str_radix(&self.rgb[0..2], 16).ok()?;
92 let r = u8::from_str_radix(&self.rgb[2..4], 16).ok()?;
93 let g = u8::from_str_radix(&self.rgb[4..6], 16).ok()?;
94 let b = u8::from_str_radix(&self.rgb[6..8], 16).ok()?;
95
96 Some((a, r, g, b))
97 }
98
99 pub fn with_alpha(&self, alpha: u8) -> Self {
101 if let Some((_, r, g, b)) = self.get_components() {
102 Self::from_argb(alpha, r, g, b)
103 } else {
104 self.clone()
105 }
106 }
107
108 pub fn is_transparent(&self) -> bool {
110 self.get_components()
111 .map(|(a, _, _, _)| a == 0)
112 .unwrap_or(false)
113 }
114}
115
116#[derive(Debug, Clone, PartialEq)]
117pub struct Border {
118 pub left: BorderStyle,
119 pub right: BorderStyle,
120 pub top: BorderStyle,
121 pub bottom: BorderStyle,
122 pub diagonal: BorderStyle,
123}
124
125#[derive(Debug, Clone, PartialEq)]
126pub struct BorderStyle {
127 pub style: Option<LineStyle>,
128 pub color: Option<Color>,
129}
130
131#[derive(Debug, Clone, PartialEq)]
132pub enum LineStyle {
133 None,
134 Thin,
135 Medium,
136 Thick,
137 Double,
138 Dotted,
139 Dashed,
140}
141
142#[derive(Debug, Clone, PartialEq)]
143pub struct Fill {
144 pub pattern_type: PatternType,
145 pub fg_color: Option<Color>,
146 pub bg_color: Option<Color>,
147}
148
149#[derive(Debug, Clone, PartialEq)]
150pub enum PatternType {
151 None,
152 Solid,
153 MediumGray,
154 DarkGray,
155 LightGray,
156 }
158
159#[derive(Debug, Clone, PartialEq)]
160pub struct Alignment {
161 pub horizontal: HorizontalAlignment,
162 pub vertical: VerticalAlignment,
163 pub wrap_text: bool,
164 pub text_rotation: i32,
165 pub indent: u32,
166}
167
168#[derive(Debug, Clone, PartialEq)]
169pub enum HorizontalAlignment {
170 Left,
171 Center,
172 Right,
173 Fill,
174 Justify,
175 CenterContinuous,
176 Distributed,
177}
178
179#[derive(Debug, Clone, PartialEq)]
180pub enum VerticalAlignment {
181 Top,
182 Center,
183 Bottom,
184 Justify,
185 Distributed,
186}
187
188#[derive(Debug, Clone)]
189pub struct CellStyle {
190 pub font: Option<Font>,
191 pub fill: Option<Fill>,
192 pub border: Option<Border>,
193 pub alignment: Option<Alignment>,
194 pub number_format: Option<String>,
195 pub protection: Option<Protection>,
196}
197
198#[derive(Debug, Clone, PartialEq)]
199pub struct Protection {
200 pub locked: bool,
201 pub hidden: bool,
202}
203
204pub struct StylesManager {
205 styles: Vec<CellStyle>,
206 style_index_map: HashMap<String, usize>,
207}
208
209impl StylesManager {
210 pub fn new() -> Self {
211 Self {
212 styles: Vec::new(),
213 style_index_map: HashMap::new(),
214 }
215 }
216
217 pub fn add_style(&mut self, style: CellStyle) -> usize {
218 let style_hash = self.compute_style_hash(&style);
219 if let Some(&index) = self.style_index_map.get(&style_hash) {
220 return index;
221 }
222
223 let index = self.styles.len();
224 self.styles.push(style);
225 self.style_index_map.insert(style_hash, index);
226 index
227 }
228
229 pub fn get_style(&self, index: usize) -> Option<&CellStyle> {
230 self.styles.get(index)
231 }
232
233 fn compute_style_hash(&self, style: &CellStyle) -> String {
234 use std::collections::hash_map::DefaultHasher;
235 use std::hash::{ Hash, Hasher };
236
237 let mut hasher = DefaultHasher::new();
238
239 if let Some(font) = &style.font {
241 font.name.hash(&mut hasher);
242 font.size.to_bits().hash(&mut hasher);
243 font.bold.hash(&mut hasher);
244 font.italic.hash(&mut hasher);
245 if let Some(color) = &font.color {
246 color.rgb.hash(&mut hasher);
247 }
248 }
249
250 if let Some(fill) = &style.fill {
252 std::mem::discriminant(&fill.pattern_type).hash(&mut hasher);
253 if let Some(fg) = &fill.fg_color {
254 fg.rgb.hash(&mut hasher);
255 }
256 if let Some(bg) = &fill.bg_color {
257 bg.rgb.hash(&mut hasher);
258 }
259 }
260
261 if let Some(border) = &style.border {
263 for side in [
264 &border.left,
265 &border.right,
266 &border.top,
267 &border.bottom,
268 &border.diagonal,
269 ] {
270 if let Some(style) = &side.style {
271 std::mem::discriminant(style).hash(&mut hasher);
272 }
273 if let Some(color) = &side.color {
274 color.rgb.hash(&mut hasher);
275 }
276 }
277 }
278
279 if let Some(align) = &style.alignment {
281 std::mem::discriminant(&align.horizontal).hash(&mut hasher);
282 std::mem::discriminant(&align.vertical).hash(&mut hasher);
283 align.wrap_text.hash(&mut hasher);
284 align.text_rotation.hash(&mut hasher);
285 align.indent.hash(&mut hasher);
286 }
287
288 format!("{:016x}", hasher.finish())
289 }
290}
291
292impl Default for CellStyle {
293 fn default() -> Self {
294 Self {
295 font: None,
296 fill: None,
297 border: None,
298 alignment: None,
299 number_format: None,
300 protection: None,
301 }
302 }
303}
304
305impl CellStyle {
306 pub fn new() -> Self {
307 Self::default()
308 }
309
310 pub fn with_font(mut self, font: Font) -> Self {
311 self.font = Some(font);
312 self
313 }
314
315 pub fn with_fill(mut self, fill: Fill) -> Self {
316 self.fill = Some(fill);
317 self
318 }
319
320 pub fn with_border(mut self, border: Border) -> Self {
321 self.border = Some(border);
322 self
323 }
324
325 pub fn with_alignment(mut self, alignment: Alignment) -> Self {
326 self.alignment = Some(alignment);
327 self
328 }
329
330 pub fn with_number_format(mut self, format: String) -> Self {
331 self.number_format = Some(format);
332 self
333 }
334
335 pub fn with_protection(mut self, protection: Protection) -> Self {
336 self.protection = Some(protection);
337 self
338 }
339
340 pub fn default_header() -> Self {
341 Self::new()
342 .with_font(Font {
343 name: "Arial".to_string(),
344 size: 12.0,
345 bold: true,
346 italic: false,
347 underline: None,
348 color: Some(Color::from_rgb(0, 0, 0)),
349 })
350 .with_alignment(Alignment {
351 horizontal: HorizontalAlignment::Center,
352 vertical: VerticalAlignment::Center,
353 wrap_text: true,
354 text_rotation: 0,
355 indent: 0,
356 })
357 .with_border(Border {
358 left: BorderStyle {
359 style: Some(LineStyle::Thin),
360 color: Some(Color::from_rgb(0, 0, 0)),
361 },
362 right: BorderStyle {
363 style: Some(LineStyle::Thin),
364 color: Some(Color::from_rgb(0, 0, 0)),
365 },
366 top: BorderStyle {
367 style: Some(LineStyle::Thin),
368 color: Some(Color::from_rgb(0, 0, 0)),
369 },
370 bottom: BorderStyle {
371 style: Some(LineStyle::Thin),
372 color: Some(Color::from_rgb(0, 0, 0)),
373 },
374 diagonal: BorderStyle {
375 style: None,
376 color: None,
377 },
378 })
379 }
380
381 pub fn default_body() -> Self {
382 Self::new()
383 .with_font(Font {
384 name: "Arial".to_string(),
385 size: 11.0,
386 bold: false,
387 italic: false,
388 underline: None,
389 color: Some(Color::from_rgb(0, 0, 0)),
390 })
391 .with_alignment(Alignment {
392 horizontal: HorizontalAlignment::Left,
393 vertical: VerticalAlignment::Center,
394 wrap_text: false,
395 text_rotation: 0,
396 indent: 0,
397 })
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_color_creation() {
407 let color = Color::from_rgb(255, 128, 0);
408 assert_eq!(color.rgb, "FFFF8000");
409
410 let color = Color::from_argb(128, 255, 128, 0);
411 assert_eq!(color.rgb, "80FF8000");
412 }
413
414 #[test]
415 fn test_style_manager() {
416 let mut manager = StylesManager::new();
417
418 let style1 = CellStyle::default_header();
419 let style2 = CellStyle::default_header();
420 let style3 = CellStyle::default_body();
421
422 let index1 = manager.add_style(style1.clone());
423 let index2 = manager.add_style(style2);
424 let index3 = manager.add_style(style3);
425
426 assert_eq!(index1, index2);
428 assert_ne!(index1, index3);
430 }
431
432 #[test]
433 fn test_color_components() {
434 let color = Color::from_argb(128, 255, 128, 0);
435 assert_eq!(color.get_components(), Some((128, 255, 128, 0)));
436 }
437
438 #[test]
439 fn test_color_constants() {
440 assert_eq!(Color::black().rgb, "FF000000");
441 assert_eq!(Color::white().rgb, "FFFFFFFF");
442 assert_eq!(Color::red().rgb, "FFFF0000");
443 }
444
445 #[test]
446 fn test_color_transparency() {
447 let color = Color::red();
448 assert!(!color.is_transparent());
449
450 let transparent = color.with_alpha(0);
451 assert!(transparent.is_transparent());
452 }
453
454 #[test]
455 fn test_invalid_color() {
456 let invalid_color = Color::new("invalid");
457 assert!(!invalid_color.is_valid());
458 assert_eq!(invalid_color.get_components(), None);
459 }
460}