Skip to main content

sheetkit_xml/
styles.rs

1//! Styles XML schema structures.
2//!
3//! Represents `xl/styles.xml` in the OOXML package.
4//! Minimal implementation for Phase 1 with Excel-compatible default styles.
5
6use serde::{Deserialize, Serialize};
7
8use crate::namespaces;
9
10/// Stylesheet root element (`xl/styles.xml`).
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[serde(rename = "styleSheet")]
13pub struct StyleSheet {
14    #[serde(rename = "@xmlns")]
15    pub xmlns: String,
16
17    #[serde(rename = "numFmts", skip_serializing_if = "Option::is_none")]
18    pub num_fmts: Option<NumFmts>,
19
20    #[serde(rename = "fonts")]
21    pub fonts: Fonts,
22
23    #[serde(rename = "fills")]
24    pub fills: Fills,
25
26    #[serde(rename = "borders")]
27    pub borders: Borders,
28
29    #[serde(rename = "cellStyleXfs", skip_serializing_if = "Option::is_none")]
30    pub cell_style_xfs: Option<CellStyleXfs>,
31
32    #[serde(rename = "cellXfs")]
33    pub cell_xfs: CellXfs,
34
35    #[serde(rename = "cellStyles", skip_serializing_if = "Option::is_none")]
36    pub cell_styles: Option<CellStyles>,
37
38    #[serde(rename = "dxfs", skip_serializing_if = "Option::is_none")]
39    pub dxfs: Option<Dxfs>,
40
41    #[serde(rename = "tableStyles", skip_serializing_if = "Option::is_none")]
42    pub table_styles: Option<TableStyles>,
43}
44
45/// Number formats container.
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct NumFmts {
48    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
49    pub count: Option<u32>,
50
51    #[serde(rename = "numFmt", default)]
52    pub num_fmts: Vec<NumFmt>,
53}
54
55/// Individual number format.
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct NumFmt {
58    #[serde(rename = "@numFmtId")]
59    pub num_fmt_id: u32,
60
61    #[serde(rename = "@formatCode")]
62    pub format_code: String,
63}
64
65/// Fonts container.
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct Fonts {
68    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
69    pub count: Option<u32>,
70
71    #[serde(rename = "font", default)]
72    pub fonts: Vec<Font>,
73}
74
75/// Individual font definition.
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct Font {
78    #[serde(rename = "b", skip_serializing_if = "Option::is_none")]
79    pub b: Option<BoolVal>,
80
81    #[serde(rename = "i", skip_serializing_if = "Option::is_none")]
82    pub i: Option<BoolVal>,
83
84    #[serde(rename = "strike", skip_serializing_if = "Option::is_none")]
85    pub strike: Option<BoolVal>,
86
87    #[serde(rename = "u", skip_serializing_if = "Option::is_none")]
88    pub u: Option<Underline>,
89
90    #[serde(rename = "sz", skip_serializing_if = "Option::is_none")]
91    pub sz: Option<FontSize>,
92
93    #[serde(rename = "color", skip_serializing_if = "Option::is_none")]
94    pub color: Option<Color>,
95
96    #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
97    pub name: Option<FontName>,
98
99    #[serde(rename = "family", skip_serializing_if = "Option::is_none")]
100    pub family: Option<FontFamily>,
101
102    #[serde(rename = "scheme", skip_serializing_if = "Option::is_none")]
103    pub scheme: Option<FontScheme>,
104}
105
106/// Fills container.
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108pub struct Fills {
109    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
110    pub count: Option<u32>,
111
112    #[serde(rename = "fill", default)]
113    pub fills: Vec<Fill>,
114}
115
116/// Individual fill definition.
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118pub struct Fill {
119    #[serde(rename = "patternFill", skip_serializing_if = "Option::is_none")]
120    pub pattern_fill: Option<PatternFill>,
121
122    #[serde(rename = "gradientFill", skip_serializing_if = "Option::is_none")]
123    pub gradient_fill: Option<GradientFill>,
124}
125
126/// Pattern fill definition.
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub struct PatternFill {
129    #[serde(rename = "@patternType", skip_serializing_if = "Option::is_none")]
130    pub pattern_type: Option<String>,
131
132    #[serde(rename = "fgColor", skip_serializing_if = "Option::is_none")]
133    pub fg_color: Option<Color>,
134
135    #[serde(rename = "bgColor", skip_serializing_if = "Option::is_none")]
136    pub bg_color: Option<Color>,
137}
138
139/// Gradient fill definition.
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub struct GradientFill {
142    /// Gradient type: "linear" or "path".
143    #[serde(rename = "@type", skip_serializing_if = "Option::is_none")]
144    pub gradient_type: Option<String>,
145
146    /// Rotation angle in degrees for linear gradients.
147    #[serde(rename = "@degree", skip_serializing_if = "Option::is_none")]
148    pub degree: Option<f64>,
149
150    /// Left coordinate for path gradients (0.0-1.0).
151    #[serde(rename = "@left", skip_serializing_if = "Option::is_none")]
152    pub left: Option<f64>,
153
154    /// Right coordinate for path gradients (0.0-1.0).
155    #[serde(rename = "@right", skip_serializing_if = "Option::is_none")]
156    pub right: Option<f64>,
157
158    /// Top coordinate for path gradients (0.0-1.0).
159    #[serde(rename = "@top", skip_serializing_if = "Option::is_none")]
160    pub top: Option<f64>,
161
162    /// Bottom coordinate for path gradients (0.0-1.0).
163    #[serde(rename = "@bottom", skip_serializing_if = "Option::is_none")]
164    pub bottom: Option<f64>,
165
166    /// Gradient stops.
167    #[serde(rename = "stop", default)]
168    pub stops: Vec<GradientStop>,
169}
170
171/// A single gradient stop with position and color.
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct GradientStop {
174    /// Position of this stop (0.0-1.0).
175    #[serde(rename = "@position")]
176    pub position: f64,
177
178    /// Color at this stop.
179    pub color: Color,
180}
181
182/// Borders container.
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184pub struct Borders {
185    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
186    pub count: Option<u32>,
187
188    #[serde(rename = "border", default)]
189    pub borders: Vec<Border>,
190}
191
192/// Individual border definition.
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub struct Border {
195    #[serde(rename = "@diagonalUp", skip_serializing_if = "Option::is_none")]
196    pub diagonal_up: Option<bool>,
197
198    #[serde(rename = "@diagonalDown", skip_serializing_if = "Option::is_none")]
199    pub diagonal_down: Option<bool>,
200
201    #[serde(rename = "left", skip_serializing_if = "Option::is_none")]
202    pub left: Option<BorderSide>,
203
204    #[serde(rename = "right", skip_serializing_if = "Option::is_none")]
205    pub right: Option<BorderSide>,
206
207    #[serde(rename = "top", skip_serializing_if = "Option::is_none")]
208    pub top: Option<BorderSide>,
209
210    #[serde(rename = "bottom", skip_serializing_if = "Option::is_none")]
211    pub bottom: Option<BorderSide>,
212
213    #[serde(rename = "diagonal", skip_serializing_if = "Option::is_none")]
214    pub diagonal: Option<BorderSide>,
215}
216
217/// Border side definition.
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219pub struct BorderSide {
220    #[serde(rename = "@style", skip_serializing_if = "Option::is_none")]
221    pub style: Option<String>,
222
223    #[serde(rename = "color", skip_serializing_if = "Option::is_none")]
224    pub color: Option<Color>,
225}
226
227/// Cell style XFs container (base style formats).
228#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
229pub struct CellStyleXfs {
230    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
231    pub count: Option<u32>,
232
233    #[serde(rename = "xf", default)]
234    pub xfs: Vec<Xf>,
235}
236
237/// Cell XFs container (applied cell formats).
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239pub struct CellXfs {
240    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
241    pub count: Option<u32>,
242
243    #[serde(rename = "xf", default)]
244    pub xfs: Vec<Xf>,
245}
246
247/// Cell format entry.
248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
249pub struct Xf {
250    #[serde(rename = "@numFmtId", skip_serializing_if = "Option::is_none")]
251    pub num_fmt_id: Option<u32>,
252
253    #[serde(rename = "@fontId", skip_serializing_if = "Option::is_none")]
254    pub font_id: Option<u32>,
255
256    #[serde(rename = "@fillId", skip_serializing_if = "Option::is_none")]
257    pub fill_id: Option<u32>,
258
259    #[serde(rename = "@borderId", skip_serializing_if = "Option::is_none")]
260    pub border_id: Option<u32>,
261
262    #[serde(rename = "@xfId", skip_serializing_if = "Option::is_none")]
263    pub xf_id: Option<u32>,
264
265    #[serde(rename = "@applyNumberFormat", skip_serializing_if = "Option::is_none")]
266    pub apply_number_format: Option<bool>,
267
268    #[serde(rename = "@applyFont", skip_serializing_if = "Option::is_none")]
269    pub apply_font: Option<bool>,
270
271    #[serde(rename = "@applyFill", skip_serializing_if = "Option::is_none")]
272    pub apply_fill: Option<bool>,
273
274    #[serde(rename = "@applyBorder", skip_serializing_if = "Option::is_none")]
275    pub apply_border: Option<bool>,
276
277    #[serde(rename = "@applyAlignment", skip_serializing_if = "Option::is_none")]
278    pub apply_alignment: Option<bool>,
279
280    #[serde(rename = "alignment", skip_serializing_if = "Option::is_none")]
281    pub alignment: Option<Alignment>,
282
283    #[serde(rename = "protection", skip_serializing_if = "Option::is_none")]
284    pub protection: Option<Protection>,
285}
286
287/// Cell alignment.
288#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
289pub struct Alignment {
290    #[serde(rename = "@horizontal", skip_serializing_if = "Option::is_none")]
291    pub horizontal: Option<String>,
292
293    #[serde(rename = "@vertical", skip_serializing_if = "Option::is_none")]
294    pub vertical: Option<String>,
295
296    #[serde(rename = "@wrapText", skip_serializing_if = "Option::is_none")]
297    pub wrap_text: Option<bool>,
298
299    #[serde(rename = "@textRotation", skip_serializing_if = "Option::is_none")]
300    pub text_rotation: Option<u32>,
301
302    #[serde(rename = "@indent", skip_serializing_if = "Option::is_none")]
303    pub indent: Option<u32>,
304
305    #[serde(rename = "@shrinkToFit", skip_serializing_if = "Option::is_none")]
306    pub shrink_to_fit: Option<bool>,
307}
308
309/// Cell protection.
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311pub struct Protection {
312    #[serde(rename = "@locked", skip_serializing_if = "Option::is_none")]
313    pub locked: Option<bool>,
314
315    #[serde(rename = "@hidden", skip_serializing_if = "Option::is_none")]
316    pub hidden: Option<bool>,
317}
318
319/// Cell styles container.
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
321pub struct CellStyles {
322    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
323    pub count: Option<u32>,
324
325    #[serde(rename = "cellStyle", default)]
326    pub cell_styles: Vec<CellStyle>,
327}
328
329/// Individual named cell style.
330#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
331pub struct CellStyle {
332    #[serde(rename = "@name")]
333    pub name: String,
334
335    #[serde(rename = "@xfId")]
336    pub xf_id: u32,
337
338    #[serde(rename = "@builtinId", skip_serializing_if = "Option::is_none")]
339    pub builtin_id: Option<u32>,
340}
341
342/// Differential formats container (for conditional formatting).
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344pub struct Dxfs {
345    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
346    pub count: Option<u32>,
347
348    #[serde(rename = "dxf", default)]
349    pub dxfs: Vec<Dxf>,
350}
351
352/// Individual differential format.
353#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
354pub struct Dxf {
355    #[serde(rename = "font", skip_serializing_if = "Option::is_none")]
356    pub font: Option<Font>,
357
358    #[serde(rename = "numFmt", skip_serializing_if = "Option::is_none")]
359    pub num_fmt: Option<NumFmt>,
360
361    #[serde(rename = "fill", skip_serializing_if = "Option::is_none")]
362    pub fill: Option<Fill>,
363
364    #[serde(rename = "border", skip_serializing_if = "Option::is_none")]
365    pub border: Option<Border>,
366}
367
368/// Table styles container.
369#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
370pub struct TableStyles {
371    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
372    pub count: Option<u32>,
373
374    #[serde(rename = "@defaultTableStyle", skip_serializing_if = "Option::is_none")]
375    pub default_table_style: Option<String>,
376
377    #[serde(rename = "@defaultPivotStyle", skip_serializing_if = "Option::is_none")]
378    pub default_pivot_style: Option<String>,
379}
380
381/// Color definition (used across fonts, fills, borders).
382#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
383pub struct Color {
384    #[serde(rename = "@auto", skip_serializing_if = "Option::is_none")]
385    pub auto: Option<bool>,
386
387    #[serde(rename = "@indexed", skip_serializing_if = "Option::is_none")]
388    pub indexed: Option<u32>,
389
390    #[serde(rename = "@rgb", skip_serializing_if = "Option::is_none")]
391    pub rgb: Option<String>,
392
393    #[serde(rename = "@theme", skip_serializing_if = "Option::is_none")]
394    pub theme: Option<u32>,
395
396    #[serde(rename = "@tint", skip_serializing_if = "Option::is_none")]
397    pub tint: Option<f64>,
398}
399
400/// Boolean value wrapper (used for bold, italic, etc.).
401#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
402pub struct BoolVal {
403    #[serde(rename = "@val", skip_serializing_if = "Option::is_none")]
404    pub val: Option<bool>,
405}
406
407/// Underline.
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
409pub struct Underline {
410    #[serde(rename = "@val", skip_serializing_if = "Option::is_none")]
411    pub val: Option<String>,
412}
413
414/// Font size.
415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
416pub struct FontSize {
417    #[serde(rename = "@val")]
418    pub val: f64,
419}
420
421/// Font name.
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
423pub struct FontName {
424    #[serde(rename = "@val")]
425    pub val: String,
426}
427
428/// Font family.
429#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
430pub struct FontFamily {
431    #[serde(rename = "@val")]
432    pub val: u32,
433}
434
435/// Font scheme (theme-based).
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
437pub struct FontScheme {
438    #[serde(rename = "@val")]
439    pub val: String,
440}
441
442impl Default for StyleSheet {
443    /// Creates an Excel-compatible minimal default stylesheet.
444    ///
445    /// This includes:
446    /// - 1 default font (Calibri, 11pt)
447    /// - 2 required fills (none, gray125)
448    /// - 1 empty border
449    /// - 1 cellStyleXf
450    /// - 1 cellXf (Normal style)
451    /// - 1 cellStyle ("Normal")
452    fn default() -> Self {
453        Self {
454            xmlns: namespaces::SPREADSHEET_ML.to_string(),
455            num_fmts: None,
456            fonts: Fonts {
457                count: Some(1),
458                fonts: vec![Font {
459                    b: None,
460                    i: None,
461                    strike: None,
462                    u: None,
463                    sz: Some(FontSize { val: 11.0 }),
464                    color: Some(Color {
465                        auto: None,
466                        indexed: None,
467                        rgb: None,
468                        theme: Some(1),
469                        tint: None,
470                    }),
471                    name: Some(FontName {
472                        val: "Calibri".to_string(),
473                    }),
474                    family: Some(FontFamily { val: 2 }),
475                    scheme: Some(FontScheme {
476                        val: "minor".to_string(),
477                    }),
478                }],
479            },
480            fills: Fills {
481                count: Some(2),
482                fills: vec![
483                    Fill {
484                        pattern_fill: Some(PatternFill {
485                            pattern_type: Some("none".to_string()),
486                            fg_color: None,
487                            bg_color: None,
488                        }),
489                        gradient_fill: None,
490                    },
491                    Fill {
492                        pattern_fill: Some(PatternFill {
493                            pattern_type: Some("gray125".to_string()),
494                            fg_color: None,
495                            bg_color: None,
496                        }),
497                        gradient_fill: None,
498                    },
499                ],
500            },
501            borders: Borders {
502                count: Some(1),
503                borders: vec![Border {
504                    diagonal_up: None,
505                    diagonal_down: None,
506                    left: Some(BorderSide {
507                        style: None,
508                        color: None,
509                    }),
510                    right: Some(BorderSide {
511                        style: None,
512                        color: None,
513                    }),
514                    top: Some(BorderSide {
515                        style: None,
516                        color: None,
517                    }),
518                    bottom: Some(BorderSide {
519                        style: None,
520                        color: None,
521                    }),
522                    diagonal: Some(BorderSide {
523                        style: None,
524                        color: None,
525                    }),
526                }],
527            },
528            cell_style_xfs: Some(CellStyleXfs {
529                count: Some(1),
530                xfs: vec![Xf {
531                    num_fmt_id: Some(0),
532                    font_id: Some(0),
533                    fill_id: Some(0),
534                    border_id: Some(0),
535                    xf_id: None,
536                    apply_number_format: None,
537                    apply_font: None,
538                    apply_fill: None,
539                    apply_border: None,
540                    apply_alignment: None,
541                    alignment: None,
542                    protection: None,
543                }],
544            }),
545            cell_xfs: CellXfs {
546                count: Some(1),
547                xfs: vec![Xf {
548                    num_fmt_id: Some(0),
549                    font_id: Some(0),
550                    fill_id: Some(0),
551                    border_id: Some(0),
552                    xf_id: Some(0),
553                    apply_number_format: None,
554                    apply_font: None,
555                    apply_fill: None,
556                    apply_border: None,
557                    apply_alignment: None,
558                    alignment: None,
559                    protection: None,
560                }],
561            },
562            cell_styles: Some(CellStyles {
563                count: Some(1),
564                cell_styles: vec![CellStyle {
565                    name: "Normal".to_string(),
566                    xf_id: 0,
567                    builtin_id: Some(0),
568                }],
569            }),
570            dxfs: None,
571            table_styles: None,
572        }
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    #[test]
581    fn test_stylesheet_default() {
582        let ss = StyleSheet::default();
583        assert_eq!(ss.xmlns, namespaces::SPREADSHEET_ML);
584
585        // 1 default font
586        assert_eq!(ss.fonts.fonts.len(), 1);
587        assert_eq!(ss.fonts.count, Some(1));
588        let font = &ss.fonts.fonts[0];
589        assert_eq!(font.sz.as_ref().unwrap().val, 11.0);
590        assert_eq!(font.name.as_ref().unwrap().val, "Calibri");
591        assert_eq!(font.family.as_ref().unwrap().val, 2);
592        assert_eq!(font.scheme.as_ref().unwrap().val, "minor");
593
594        // 2 required fills (none, gray125)
595        assert_eq!(ss.fills.fills.len(), 2);
596        assert_eq!(ss.fills.count, Some(2));
597        assert_eq!(
598            ss.fills.fills[0]
599                .pattern_fill
600                .as_ref()
601                .unwrap()
602                .pattern_type,
603            Some("none".to_string())
604        );
605        assert_eq!(
606            ss.fills.fills[1]
607                .pattern_fill
608                .as_ref()
609                .unwrap()
610                .pattern_type,
611            Some("gray125".to_string())
612        );
613
614        // 1 border
615        assert_eq!(ss.borders.borders.len(), 1);
616        assert_eq!(ss.borders.count, Some(1));
617
618        // 1 cellStyleXf
619        assert!(ss.cell_style_xfs.is_some());
620        assert_eq!(ss.cell_style_xfs.as_ref().unwrap().xfs.len(), 1);
621
622        // 1 cellXf
623        assert_eq!(ss.cell_xfs.xfs.len(), 1);
624        assert_eq!(ss.cell_xfs.count, Some(1));
625        let xf = &ss.cell_xfs.xfs[0];
626        assert_eq!(xf.num_fmt_id, Some(0));
627        assert_eq!(xf.font_id, Some(0));
628        assert_eq!(xf.fill_id, Some(0));
629        assert_eq!(xf.border_id, Some(0));
630
631        // 1 cellStyle
632        assert!(ss.cell_styles.is_some());
633        let styles = ss.cell_styles.as_ref().unwrap();
634        assert_eq!(styles.cell_styles.len(), 1);
635        assert_eq!(styles.cell_styles[0].name, "Normal");
636        assert_eq!(styles.cell_styles[0].xf_id, 0);
637        assert_eq!(styles.cell_styles[0].builtin_id, Some(0));
638    }
639
640    #[test]
641    fn test_stylesheet_roundtrip() {
642        let ss = StyleSheet::default();
643        let xml = quick_xml::se::to_string(&ss).unwrap();
644        let parsed: StyleSheet = quick_xml::de::from_str(&xml).unwrap();
645
646        assert_eq!(ss.xmlns, parsed.xmlns);
647        assert_eq!(ss.fonts.fonts.len(), parsed.fonts.fonts.len());
648        assert_eq!(ss.fills.fills.len(), parsed.fills.fills.len());
649        assert_eq!(ss.borders.borders.len(), parsed.borders.borders.len());
650        assert_eq!(ss.cell_xfs.xfs.len(), parsed.cell_xfs.xfs.len());
651    }
652
653    #[test]
654    fn test_stylesheet_serialize_structure() {
655        let ss = StyleSheet::default();
656        let xml = quick_xml::se::to_string(&ss).unwrap();
657        assert!(xml.contains("<styleSheet"));
658        assert!(xml.contains("<fonts"));
659        assert!(xml.contains("<font>"));
660        assert!(xml.contains("<fills"));
661        assert!(xml.contains("<fill>"));
662        assert!(xml.contains("<borders"));
663        assert!(xml.contains("<border>"));
664        assert!(xml.contains("<cellXfs"));
665        assert!(xml.contains("<xf "));
666    }
667
668    #[test]
669    fn test_parse_real_excel_styles_minimal() {
670        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
671<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
672  <fonts count="1">
673    <font>
674      <sz val="11"/>
675      <color theme="1"/>
676      <name val="Calibri"/>
677      <family val="2"/>
678      <scheme val="minor"/>
679    </font>
680  </fonts>
681  <fills count="2">
682    <fill><patternFill patternType="none"/></fill>
683    <fill><patternFill patternType="gray125"/></fill>
684  </fills>
685  <borders count="1">
686    <border>
687      <left/>
688      <right/>
689      <top/>
690      <bottom/>
691      <diagonal/>
692    </border>
693  </borders>
694  <cellStyleXfs count="1">
695    <xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>
696  </cellStyleXfs>
697  <cellXfs count="1">
698    <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
699  </cellXfs>
700  <cellStyles count="1">
701    <cellStyle name="Normal" xfId="0" builtinId="0"/>
702  </cellStyles>
703</styleSheet>"#;
704
705        let parsed: StyleSheet = quick_xml::de::from_str(xml).unwrap();
706        assert_eq!(parsed.fonts.fonts.len(), 1);
707        assert_eq!(parsed.fonts.fonts[0].sz.as_ref().unwrap().val, 11.0);
708        assert_eq!(parsed.fonts.fonts[0].name.as_ref().unwrap().val, "Calibri");
709        assert_eq!(parsed.fills.fills.len(), 2);
710        assert_eq!(parsed.borders.borders.len(), 1);
711        assert_eq!(parsed.cell_xfs.xfs.len(), 1);
712        assert_eq!(parsed.cell_xfs.xfs[0].num_fmt_id, Some(0));
713    }
714
715    #[test]
716    fn test_font_with_bold_italic() {
717        let font = Font {
718            b: Some(BoolVal { val: None }),
719            i: Some(BoolVal { val: None }),
720            strike: None,
721            u: None,
722            sz: Some(FontSize { val: 12.0 }),
723            color: None,
724            name: Some(FontName {
725                val: "Arial".to_string(),
726            }),
727            family: None,
728            scheme: None,
729        };
730        let xml = quick_xml::se::to_string(&font).unwrap();
731        assert!(xml.contains("<b"));
732        assert!(xml.contains("<i"));
733        let parsed: Font = quick_xml::de::from_str(&xml).unwrap();
734        assert!(parsed.b.is_some());
735        assert!(parsed.i.is_some());
736        assert_eq!(parsed.sz.unwrap().val, 12.0);
737    }
738
739    #[test]
740    fn test_color_rgb() {
741        let color = Color {
742            auto: None,
743            indexed: None,
744            rgb: Some("FF0000FF".to_string()),
745            theme: None,
746            tint: None,
747        };
748        let xml = quick_xml::se::to_string(&color).unwrap();
749        assert!(xml.contains("rgb=\"FF0000FF\""));
750        let parsed: Color = quick_xml::de::from_str(&xml).unwrap();
751        assert_eq!(parsed.rgb, Some("FF0000FF".to_string()));
752    }
753
754    #[test]
755    fn test_color_theme_with_tint() {
756        let color = Color {
757            auto: None,
758            indexed: None,
759            rgb: None,
760            theme: Some(4),
761            tint: Some(0.399_975_585_192_419_2),
762        };
763        let xml = quick_xml::se::to_string(&color).unwrap();
764        let parsed: Color = quick_xml::de::from_str(&xml).unwrap();
765        assert_eq!(parsed.theme, Some(4));
766        assert!(parsed.tint.is_some());
767    }
768
769    #[test]
770    fn test_border_with_style() {
771        let border = Border {
772            diagonal_up: None,
773            diagonal_down: None,
774            left: Some(BorderSide {
775                style: Some("thin".to_string()),
776                color: Some(Color {
777                    auto: Some(true),
778                    indexed: None,
779                    rgb: None,
780                    theme: None,
781                    tint: None,
782                }),
783            }),
784            right: None,
785            top: None,
786            bottom: None,
787            diagonal: None,
788        };
789        let xml = quick_xml::se::to_string(&border).unwrap();
790        assert!(xml.contains("style=\"thin\""));
791        let parsed: Border = quick_xml::de::from_str(&xml).unwrap();
792        assert_eq!(
793            parsed.left.as_ref().unwrap().style,
794            Some("thin".to_string())
795        );
796        assert_eq!(
797            parsed.left.as_ref().unwrap().color.as_ref().unwrap().auto,
798            Some(true)
799        );
800    }
801
802    #[test]
803    fn test_xf_with_alignment() {
804        let xf = Xf {
805            num_fmt_id: Some(0),
806            font_id: Some(0),
807            fill_id: Some(0),
808            border_id: Some(0),
809            xf_id: Some(0),
810            apply_number_format: None,
811            apply_font: None,
812            apply_fill: None,
813            apply_border: None,
814            apply_alignment: Some(true),
815            alignment: Some(Alignment {
816                horizontal: Some("center".to_string()),
817                vertical: Some("center".to_string()),
818                wrap_text: Some(true),
819                text_rotation: None,
820                indent: None,
821                shrink_to_fit: None,
822            }),
823            protection: None,
824        };
825        let xml = quick_xml::se::to_string(&xf).unwrap();
826        assert!(xml.contains("alignment"));
827        assert!(xml.contains("horizontal=\"center\""));
828        let parsed: Xf = quick_xml::de::from_str(&xml).unwrap();
829        assert!(parsed.alignment.is_some());
830        let align = parsed.alignment.unwrap();
831        assert_eq!(align.horizontal, Some("center".to_string()));
832        assert_eq!(align.vertical, Some("center".to_string()));
833        assert_eq!(align.wrap_text, Some(true));
834    }
835
836    #[test]
837    fn test_num_fmt() {
838        let nf = NumFmt {
839            num_fmt_id: 164,
840            format_code: "#,##0.00_ ".to_string(),
841        };
842        let xml = quick_xml::se::to_string(&nf).unwrap();
843        let parsed: NumFmt = quick_xml::de::from_str(&xml).unwrap();
844        assert_eq!(parsed.num_fmt_id, 164);
845        assert_eq!(parsed.format_code, "#,##0.00_ ");
846    }
847
848    #[test]
849    fn test_optional_fields_not_serialized() {
850        let ss = StyleSheet::default();
851        let xml = quick_xml::se::to_string(&ss).unwrap();
852        // numFmts is None, so it should not appear
853        assert!(!xml.contains("numFmts"));
854        // dxfs is None
855        assert!(!xml.contains("dxfs"));
856        // tableStyles is None
857        assert!(!xml.contains("tableStyles"));
858    }
859
860    #[test]
861    fn test_cell_style_roundtrip() {
862        let style = CellStyle {
863            name: "Heading 1".to_string(),
864            xf_id: 1,
865            builtin_id: Some(16),
866        };
867        let xml = quick_xml::se::to_string(&style).unwrap();
868        let parsed: CellStyle = quick_xml::de::from_str(&xml).unwrap();
869        assert_eq!(parsed.name, "Heading 1");
870        assert_eq!(parsed.xf_id, 1);
871        assert_eq!(parsed.builtin_id, Some(16));
872    }
873}