mod xml;
pub use xml::*;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct Font {
pub name: String,
pub size: f64,
pub bold: bool,
pub italic: bool,
pub underline: Option<UnderlineStyle>,
pub color: Option<Color>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum UnderlineStyle {
Single,
Double,
SingleAccounting,
DoubleAccounting,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Color {
pub rgb: String, }
impl Color {
pub fn new(rgb: &str) -> Self {
Self {
rgb: rgb.to_string(),
}
}
pub fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self {
rgb: format!("FF{:02X}{:02X}{:02X}", r, g, b),
}
}
pub fn from_argb(a: u8, r: u8, g: u8, b: u8) -> Self {
Self {
rgb: format!("{:02X}{:02X}{:02X}{:02X}", a, r, g, b),
}
}
pub fn is_valid(&self) -> bool {
self.rgb.len() == 8 && self.rgb.chars().all(|c| c.is_ascii_hexdigit())
}
pub const BLACK: &'static str = "FF000000";
pub const WHITE: &'static str = "FFFFFFFF";
pub const RED: &'static str = "FFFF0000";
pub const GREEN: &'static str = "FF00FF00";
pub const BLUE: &'static str = "FF0000FF";
pub const GRAY: &'static str = "FF808080";
pub fn black() -> Self {
Self::new(Self::BLACK)
}
pub fn white() -> Self {
Self::new(Self::WHITE)
}
pub fn red() -> Self {
Self::new(Self::RED)
}
pub fn green() -> Self {
Self::new(Self::GREEN)
}
pub fn blue() -> Self {
Self::new(Self::BLUE)
}
pub fn gray() -> Self {
Self::new(Self::GRAY)
}
pub fn get_components(&self) -> Option<(u8, u8, u8, u8)> {
if !self.is_valid() {
return None;
}
let a = u8::from_str_radix(&self.rgb[0..2], 16).ok()?;
let r = u8::from_str_radix(&self.rgb[2..4], 16).ok()?;
let g = u8::from_str_radix(&self.rgb[4..6], 16).ok()?;
let b = u8::from_str_radix(&self.rgb[6..8], 16).ok()?;
Some((a, r, g, b))
}
pub fn with_alpha(&self, alpha: u8) -> Self {
if let Some((_, r, g, b)) = self.get_components() {
Self::from_argb(alpha, r, g, b)
} else {
self.clone()
}
}
pub fn is_transparent(&self) -> bool {
self.get_components()
.map(|(a, _, _, _)| a == 0)
.unwrap_or(false)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Border {
pub left: BorderStyle,
pub right: BorderStyle,
pub top: BorderStyle,
pub bottom: BorderStyle,
pub diagonal: BorderStyle,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BorderStyle {
pub style: Option<LineStyle>,
pub color: Option<Color>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LineStyle {
None,
Thin,
Medium,
Thick,
Double,
Dotted,
Dashed,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Fill {
pub pattern_type: PatternType,
pub fg_color: Option<Color>,
pub bg_color: Option<Color>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PatternType {
None,
Solid,
MediumGray,
DarkGray,
LightGray,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Alignment {
pub horizontal: HorizontalAlignment,
pub vertical: VerticalAlignment,
pub wrap_text: bool,
pub text_rotation: i32,
pub indent: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HorizontalAlignment {
Left,
Center,
Right,
Fill,
Justify,
CenterContinuous,
Distributed,
}
#[derive(Debug, Clone, PartialEq)]
pub enum VerticalAlignment {
Top,
Center,
Bottom,
Justify,
Distributed,
}
#[derive(Debug, Clone)]
pub struct CellStyle {
pub font: Option<Font>,
pub fill: Option<Fill>,
pub border: Option<Border>,
pub alignment: Option<Alignment>,
pub number_format: Option<String>,
pub protection: Option<Protection>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Protection {
pub locked: bool,
pub hidden: bool,
}
pub struct StylesManager {
styles: Vec<CellStyle>,
style_index_map: HashMap<String, usize>,
}
impl StylesManager {
pub fn new() -> Self {
Self {
styles: Vec::new(),
style_index_map: HashMap::new(),
}
}
pub fn add_style(&mut self, style: CellStyle) -> usize {
let style_hash = self.compute_style_hash(&style);
if let Some(&index) = self.style_index_map.get(&style_hash) {
return index;
}
let index = self.styles.len();
self.styles.push(style);
self.style_index_map.insert(style_hash, index);
index
}
pub fn get_style(&self, index: usize) -> Option<&CellStyle> {
self.styles.get(index)
}
fn compute_style_hash(&self, style: &CellStyle) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{ Hash, Hasher };
let mut hasher = DefaultHasher::new();
if let Some(font) = &style.font {
font.name.hash(&mut hasher);
font.size.to_bits().hash(&mut hasher);
font.bold.hash(&mut hasher);
font.italic.hash(&mut hasher);
if let Some(color) = &font.color {
color.rgb.hash(&mut hasher);
}
}
if let Some(fill) = &style.fill {
std::mem::discriminant(&fill.pattern_type).hash(&mut hasher);
if let Some(fg) = &fill.fg_color {
fg.rgb.hash(&mut hasher);
}
if let Some(bg) = &fill.bg_color {
bg.rgb.hash(&mut hasher);
}
}
if let Some(border) = &style.border {
for side in [
&border.left,
&border.right,
&border.top,
&border.bottom,
&border.diagonal,
] {
if let Some(style) = &side.style {
std::mem::discriminant(style).hash(&mut hasher);
}
if let Some(color) = &side.color {
color.rgb.hash(&mut hasher);
}
}
}
if let Some(align) = &style.alignment {
std::mem::discriminant(&align.horizontal).hash(&mut hasher);
std::mem::discriminant(&align.vertical).hash(&mut hasher);
align.wrap_text.hash(&mut hasher);
align.text_rotation.hash(&mut hasher);
align.indent.hash(&mut hasher);
}
format!("{:016x}", hasher.finish())
}
}
impl Default for CellStyle {
fn default() -> Self {
Self {
font: None,
fill: None,
border: None,
alignment: None,
number_format: None,
protection: None,
}
}
}
impl CellStyle {
pub fn new() -> Self {
Self::default()
}
pub fn with_font(mut self, font: Font) -> Self {
self.font = Some(font);
self
}
pub fn with_fill(mut self, fill: Fill) -> Self {
self.fill = Some(fill);
self
}
pub fn with_border(mut self, border: Border) -> Self {
self.border = Some(border);
self
}
pub fn with_alignment(mut self, alignment: Alignment) -> Self {
self.alignment = Some(alignment);
self
}
pub fn with_number_format(mut self, format: String) -> Self {
self.number_format = Some(format);
self
}
pub fn with_protection(mut self, protection: Protection) -> Self {
self.protection = Some(protection);
self
}
pub fn default_header() -> Self {
Self::new()
.with_font(Font {
name: "Arial".to_string(),
size: 12.0,
bold: true,
italic: false,
underline: None,
color: Some(Color::from_rgb(0, 0, 0)),
})
.with_alignment(Alignment {
horizontal: HorizontalAlignment::Center,
vertical: VerticalAlignment::Center,
wrap_text: true,
text_rotation: 0,
indent: 0,
})
.with_border(Border {
left: BorderStyle {
style: Some(LineStyle::Thin),
color: Some(Color::from_rgb(0, 0, 0)),
},
right: BorderStyle {
style: Some(LineStyle::Thin),
color: Some(Color::from_rgb(0, 0, 0)),
},
top: BorderStyle {
style: Some(LineStyle::Thin),
color: Some(Color::from_rgb(0, 0, 0)),
},
bottom: BorderStyle {
style: Some(LineStyle::Thin),
color: Some(Color::from_rgb(0, 0, 0)),
},
diagonal: BorderStyle {
style: None,
color: None,
},
})
}
pub fn default_body() -> Self {
Self::new()
.with_font(Font {
name: "Arial".to_string(),
size: 11.0,
bold: false,
italic: false,
underline: None,
color: Some(Color::from_rgb(0, 0, 0)),
})
.with_alignment(Alignment {
horizontal: HorizontalAlignment::Left,
vertical: VerticalAlignment::Center,
wrap_text: false,
text_rotation: 0,
indent: 0,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_creation() {
let color = Color::from_rgb(255, 128, 0);
assert_eq!(color.rgb, "FFFF8000");
let color = Color::from_argb(128, 255, 128, 0);
assert_eq!(color.rgb, "80FF8000");
}
#[test]
fn test_style_manager() {
let mut manager = StylesManager::new();
let style1 = CellStyle::default_header();
let style2 = CellStyle::default_header();
let style3 = CellStyle::default_body();
let index1 = manager.add_style(style1.clone());
let index2 = manager.add_style(style2);
let index3 = manager.add_style(style3);
assert_eq!(index1, index2);
assert_ne!(index1, index3);
}
#[test]
fn test_color_components() {
let color = Color::from_argb(128, 255, 128, 0);
assert_eq!(color.get_components(), Some((128, 255, 128, 0)));
}
#[test]
fn test_color_constants() {
assert_eq!(Color::black().rgb, "FF000000");
assert_eq!(Color::white().rgb, "FFFFFFFF");
assert_eq!(Color::red().rgb, "FFFF0000");
}
#[test]
fn test_color_transparency() {
let color = Color::red();
assert!(!color.is_transparent());
let transparent = color.with_alpha(0);
assert!(transparent.is_transparent());
}
#[test]
fn test_invalid_color() {
let invalid_color = Color::new("invalid");
assert!(!invalid_color.is_valid());
assert_eq!(invalid_color.get_components(), None);
}
}