Skip to main content

xlsbye_xml/
styles.rs

1use crate::writer::{Result, XmlWriter};
2use std::io::Write;
3use xlsbye_core::types::{
4    Alignment, Border, BorderSide, BorderStyle, Color, ColorType, DxfStyle, Fill, Font,
5    FontScheme, GradientFill, GradientType, HorizontalAlign, NumFmt, PatternFill, PatternType,
6    Stylesheet, SuperSub, UnderlineStyle, VerticalAlign, Xf,
7};
8use xlsbye_core::xml_names::SPREADSHEET_NS;
9
10pub fn write_styles(writer: impl Write, stylesheet: &Stylesheet) -> Result<()> {
11    let mut writer = XmlWriter::new(writer);
12    writer.write_xml_declaration()?;
13    writer.write_start_element_with_ns(
14        "styleSheet",
15        [("", SPREADSHEET_NS)],
16        std::iter::empty::<(&str, &str)>(),
17    )?;
18
19    let custom_num_fmts: Vec<&NumFmt> = stylesheet
20        .num_fmts
21        .iter()
22        .filter(|fmt| fmt.id > 163)
23        .collect();
24    writer.write_start_element(
25        "numFmts",
26        [("count", custom_num_fmts.len().to_string())],
27    )?;
28    for num_fmt in custom_num_fmts {
29        writer.write_empty_element(
30            "numFmt",
31            [
32                ("numFmtId".to_string(), num_fmt.id.to_string()),
33                ("formatCode".to_string(), num_fmt.format_code.clone()),
34            ],
35        )?;
36    }
37    writer.write_end_element("numFmts")?;
38
39    writer.write_start_element("fonts", [("count", stylesheet.fonts.len().to_string())])?;
40    for font in &stylesheet.fonts {
41        write_font(&mut writer, font)?;
42    }
43    writer.write_end_element("fonts")?;
44
45    writer.write_start_element("fills", [("count", stylesheet.fills.len().to_string())])?;
46    for fill in &stylesheet.fills {
47        write_fill(&mut writer, fill)?;
48    }
49    writer.write_end_element("fills")?;
50
51    writer.write_start_element(
52        "borders",
53        [("count", stylesheet.borders.len().to_string())],
54    )?;
55    for border in &stylesheet.borders {
56        write_border(&mut writer, border)?;
57    }
58    writer.write_end_element("borders")?;
59
60    writer.write_start_element(
61        "cellStyleXfs",
62        [("count", stylesheet.cell_style_xfs.len().to_string())],
63    )?;
64    for xf in &stylesheet.cell_style_xfs {
65        write_xf(&mut writer, xf)?;
66    }
67    writer.write_end_element("cellStyleXfs")?;
68
69    writer.write_start_element(
70        "cellXfs",
71        [("count", stylesheet.cell_xfs.len().to_string())],
72    )?;
73    for xf in &stylesheet.cell_xfs {
74        write_xf(&mut writer, xf)?;
75    }
76    writer.write_end_element("cellXfs")?;
77
78    write_cell_styles(&mut writer, stylesheet.cell_style_xfs.len())?;
79
80    writer.write_start_element("dxfs", [("count", stylesheet.dxfs.len().to_string())])?;
81    for dxf in &stylesheet.dxfs {
82        write_dxf(&mut writer, dxf)?;
83    }
84    writer.write_end_element("dxfs")?;
85
86    writer.write_empty_element(
87        "tableStyles",
88        [
89            ("count", "0"),
90            ("defaultTableStyle", "TableStyleMedium9"),
91            ("defaultPivotStyle", "PivotStyleLight16"),
92        ],
93    )?;
94
95    writer.write_end_element("styleSheet")?;
96    Ok(())
97}
98
99fn write_cell_styles<W: Write>(writer: &mut XmlWriter<W>, cell_style_xf_count: usize) -> Result<()> {
100    let count = cell_style_xf_count.max(1);
101    writer.write_start_element("cellStyles", [("count", count.to_string())])?;
102
103    for index in 0..count {
104        let mut attrs = vec![
105            (
106                "name".to_string(),
107                if index == 0 {
108                    "Normal".to_string()
109                } else {
110                    format!("Style {}", index)
111                },
112            ),
113            ("xfId".to_string(), index.to_string()),
114        ];
115        if index == 0 {
116            attrs.push(("builtinId".to_string(), "0".to_string()));
117        }
118        writer.write_empty_element("cellStyle", attrs)?;
119    }
120
121    writer.write_end_element("cellStyles")
122}
123
124fn write_font<W: Write>(writer: &mut XmlWriter<W>, font: &Font) -> Result<()> {
125    writer.write_start_element("font", std::iter::empty::<(&str, &str)>())?;
126
127    if font.bold {
128        writer.write_empty_element("b", std::iter::empty::<(&str, &str)>())?;
129    }
130    if font.italic {
131        writer.write_empty_element("i", std::iter::empty::<(&str, &str)>())?;
132    }
133    if font.strikethrough {
134        writer.write_empty_element("strike", std::iter::empty::<(&str, &str)>())?;
135    }
136
137    match font.underline {
138        UnderlineStyle::None => {}
139        UnderlineStyle::Single => {
140            writer.write_empty_element("u", std::iter::empty::<(&str, &str)>())?
141        }
142        UnderlineStyle::Double => writer.write_empty_element("u", [("val", "double")])?,
143        UnderlineStyle::SingleAccounting => {
144            writer.write_empty_element("u", [("val", "singleAccounting")])?
145        }
146        UnderlineStyle::DoubleAccounting => {
147            writer.write_empty_element("u", [("val", "doubleAccounting")])?
148        }
149    }
150
151    match font.superscript {
152        SuperSub::None => {}
153        SuperSub::Superscript => {
154            writer.write_empty_element("vertAlign", [("val", "superscript")])?
155        }
156        SuperSub::Subscript => writer.write_empty_element("vertAlign", [("val", "subscript")])?,
157    }
158
159    writer.write_empty_element("sz", [("val", (font.size_twips as f64 / 20.0).to_string())])?;
160    write_color_element(writer, "color", &font.color)?;
161    writer.write_empty_element("name", [("val", font.name.as_str())])?;
162    writer.write_empty_element("family", [("val", font.family.to_string())])?;
163    if font.charset != 0 {
164        writer.write_empty_element("charset", [("val", font.charset.to_string())])?;
165    }
166
167    match font.scheme {
168        FontScheme::None => {}
169        FontScheme::Major => writer.write_empty_element("scheme", [("val", "major")])?,
170        FontScheme::Minor => writer.write_empty_element("scheme", [("val", "minor")])?,
171    }
172
173    writer.write_end_element("font")
174}
175
176fn write_fill<W: Write>(writer: &mut XmlWriter<W>, fill: &Fill) -> Result<()> {
177    writer.write_start_element("fill", std::iter::empty::<(&str, &str)>())?;
178    match &fill.pattern {
179        PatternFill::None => {
180            writer.write_empty_element("patternFill", [("patternType", "none")])?;
181        }
182        PatternFill::Solid { fg_color, bg_color } => {
183            writer.write_start_element("patternFill", [("patternType", "solid")])?;
184            write_color_element(writer, "fgColor", fg_color)?;
185            write_color_element(writer, "bgColor", bg_color)?;
186            writer.write_end_element("patternFill")?;
187        }
188        PatternFill::Pattern {
189            pattern_type,
190            fg_color,
191            bg_color,
192        } => {
193            let has_meaningful_colors = !is_default_color(fg_color) || !is_default_color(bg_color);
194            if has_meaningful_colors {
195                writer.write_start_element(
196                    "patternFill",
197                    [("patternType", pattern_type_to_str(*pattern_type))],
198                )?;
199                if !is_default_color(fg_color) {
200                    write_color_element(writer, "fgColor", fg_color)?;
201                }
202                if !is_default_color(bg_color) {
203                    write_color_element(writer, "bgColor", bg_color)?;
204                }
205                writer.write_end_element("patternFill")?;
206            } else {
207                writer.write_empty_element("patternFill", [("patternType", pattern_type_to_str(*pattern_type))])?;
208            }
209        }
210        PatternFill::Gradient(gradient) => {
211            write_gradient_fill(writer, gradient)?;
212        }
213    }
214    writer.write_end_element("fill")
215}
216
217fn write_gradient_fill<W: Write>(writer: &mut XmlWriter<W>, gradient: &GradientFill) -> Result<()> {
218    let mut attrs = vec![(
219        "type".to_string(),
220        match gradient.gradient_type {
221            GradientType::Linear => "linear".to_string(),
222            GradientType::Path => "path".to_string(),
223        },
224    )];
225
226    if gradient.degree != 0.0 {
227        attrs.push(("degree".to_string(), gradient.degree.to_string()));
228    }
229    if gradient.left != 0.0 {
230        attrs.push(("left".to_string(), gradient.left.to_string()));
231    }
232    if gradient.right != 0.0 {
233        attrs.push(("right".to_string(), gradient.right.to_string()));
234    }
235    if gradient.top != 0.0 {
236        attrs.push(("top".to_string(), gradient.top.to_string()));
237    }
238    if gradient.bottom != 0.0 {
239        attrs.push(("bottom".to_string(), gradient.bottom.to_string()));
240    }
241
242    writer.write_start_element("gradientFill", attrs)?;
243    for stop in &gradient.stops {
244        writer.write_start_element("stop", [("position", stop.position.to_string())])?;
245        write_color_element(writer, "color", &stop.color)?;
246        writer.write_end_element("stop")?;
247    }
248    writer.write_end_element("gradientFill")
249}
250
251fn write_border<W: Write>(writer: &mut XmlWriter<W>, border: &Border) -> Result<()> {
252    let mut attrs = Vec::new();
253    if border.diagonal_down {
254        attrs.push(("diagonalDown".to_string(), "1".to_string()));
255    }
256    if border.diagonal_up {
257        attrs.push(("diagonalUp".to_string(), "1".to_string()));
258    }
259    writer.write_start_element("border", attrs)?;
260
261    write_border_side(writer, "left", &border.left)?;
262    write_border_side(writer, "right", &border.right)?;
263    write_border_side(writer, "top", &border.top)?;
264    write_border_side(writer, "bottom", &border.bottom)?;
265    write_border_side(writer, "diagonal", &border.diagonal)?;
266
267    writer.write_end_element("border")
268}
269
270fn write_border_side<W: Write>(writer: &mut XmlWriter<W>, name: &str, side: &BorderSide) -> Result<()> {
271    if side.style == BorderStyle::None {
272        writer.write_empty_element(name, std::iter::empty::<(&str, &str)>())?;
273        return Ok(());
274    }
275
276    writer.write_start_element(name, [("style", border_style_to_str(side.style))])?;
277    write_color_element(writer, "color", &side.color)?;
278    writer.write_end_element(name)
279}
280
281fn write_xf<W: Write>(writer: &mut XmlWriter<W>, xf: &Xf) -> Result<()> {
282    let mut attrs = vec![
283        ("numFmtId".to_string(), xf.num_fmt_id.to_string()),
284        ("fontId".to_string(), xf.font_id.to_string()),
285        ("fillId".to_string(), xf.fill_id.to_string()),
286        ("borderId".to_string(), xf.border_id.to_string()),
287    ];
288
289    if let Some(xf_id) = xf.xf_id {
290        attrs.push(("xfId".to_string(), xf_id.to_string()));
291    }
292    if xf.apply_number_format {
293        attrs.push(("applyNumberFormat".to_string(), "1".to_string()));
294    }
295    if xf.apply_font {
296        attrs.push(("applyFont".to_string(), "1".to_string()));
297    }
298    if xf.apply_fill {
299        attrs.push(("applyFill".to_string(), "1".to_string()));
300    }
301    if xf.apply_border {
302        attrs.push(("applyBorder".to_string(), "1".to_string()));
303    }
304    if xf.apply_alignment {
305        attrs.push(("applyAlignment".to_string(), "1".to_string()));
306    }
307    if xf.apply_protection {
308        attrs.push(("applyProtection".to_string(), "1".to_string()));
309    }
310
311    let has_alignment = has_alignment(&xf.alignment);
312    let has_protection = xf.protection.locked || xf.protection.hidden;
313
314    if !has_alignment && !has_protection {
315        writer.write_empty_element("xf", attrs)?;
316        return Ok(());
317    }
318
319    writer.write_start_element("xf", attrs)?;
320    if has_alignment {
321        write_alignment(writer, &xf.alignment)?;
322    }
323    if has_protection {
324        let mut protection_attrs = Vec::new();
325        if xf.protection.locked {
326            protection_attrs.push(("locked".to_string(), "1".to_string()));
327        }
328        if xf.protection.hidden {
329            protection_attrs.push(("hidden".to_string(), "1".to_string()));
330        }
331        writer.write_empty_element("protection", protection_attrs)?;
332    }
333    writer.write_end_element("xf")
334}
335
336fn write_alignment<W: Write>(writer: &mut XmlWriter<W>, alignment: &Alignment) -> Result<()> {
337    let mut attrs = Vec::new();
338    if alignment.horizontal != HorizontalAlign::General {
339        attrs.push((
340            "horizontal".to_string(),
341            horizontal_to_str(alignment.horizontal).to_string(),
342        ));
343    }
344    if alignment.vertical != VerticalAlign::Bottom {
345        attrs.push((
346            "vertical".to_string(),
347            vertical_to_str(alignment.vertical).to_string(),
348        ));
349    }
350    if alignment.wrap_text {
351        attrs.push(("wrapText".to_string(), "1".to_string()));
352    }
353    if alignment.shrink_to_fit {
354        attrs.push(("shrinkToFit".to_string(), "1".to_string()));
355    }
356    if alignment.text_rotation != 0 {
357        attrs.push((
358            "textRotation".to_string(),
359            alignment.text_rotation.to_string(),
360        ));
361    }
362    if alignment.indent != 0 {
363        attrs.push(("indent".to_string(), alignment.indent.to_string()));
364    }
365    if alignment.reading_order != 0 {
366        attrs.push((
367            "readingOrder".to_string(),
368            alignment.reading_order.to_string(),
369        ));
370    }
371    writer.write_empty_element("alignment", attrs)
372}
373
374fn write_dxf<W: Write>(writer: &mut XmlWriter<W>, dxf: &DxfStyle) -> Result<()> {
375    writer.write_start_element("dxf", std::iter::empty::<(&str, &str)>())?;
376    if let Some(font) = &dxf.font {
377        write_font(writer, font)?;
378    }
379    if let Some(fill) = &dxf.fill {
380        write_fill(writer, fill)?;
381    }
382    if let Some(border) = &dxf.border {
383        write_border(writer, border)?;
384    }
385    if let Some(num_fmt) = &dxf.num_fmt {
386        writer.write_empty_element(
387            "numFmt",
388            [
389                ("numFmtId".to_string(), num_fmt.id.to_string()),
390                ("formatCode".to_string(), num_fmt.format_code.clone()),
391            ],
392        )?;
393    }
394    if let Some(alignment) = &dxf.alignment {
395        write_alignment(writer, alignment)?;
396    }
397    writer.write_end_element("dxf")
398}
399
400fn is_default_color(color: &Color) -> bool {
401    matches!(color.color_type, ColorType::Auto | ColorType::Indexed(64) | ColorType::Indexed(65))
402}
403
404fn write_color_element<W: Write>(writer: &mut XmlWriter<W>, name: &str, color: &Color) -> Result<()> {
405    match &color.color_type {
406        ColorType::Auto => writer.write_empty_element(name, [("auto", "1")]),
407        ColorType::Indexed(indexed) => {
408            writer.write_empty_element(name, [("indexed", indexed.to_string())])
409        }
410        ColorType::Rgb(a, r, g, b) => {
411            writer.write_empty_element(name, [("rgb", format!("{:02X}{:02X}{:02X}{:02X}", a, r, g, b))])
412        }
413        ColorType::Theme { theme, tint } => {
414            if *tint == 0.0 {
415                writer.write_empty_element(name, [("theme", theme.to_string())])
416            } else {
417                writer.write_empty_element(
418                    name,
419                    [
420                        ("theme".to_string(), theme.to_string()),
421                        ("tint".to_string(), tint.to_string()),
422                    ],
423                )
424            }
425        }
426    }
427}
428
429fn has_alignment(alignment: &Alignment) -> bool {
430    alignment.horizontal != HorizontalAlign::General
431        || alignment.vertical != VerticalAlign::Bottom
432        || alignment.wrap_text
433        || alignment.shrink_to_fit
434        || alignment.text_rotation != 0
435        || alignment.indent != 0
436        || alignment.reading_order != 0
437}
438
439fn pattern_type_to_str(pattern_type: PatternType) -> &'static str {
440    match pattern_type {
441        PatternType::DarkDown => "darkDown",
442        PatternType::DarkGray => "darkGray",
443        PatternType::DarkGrid => "darkGrid",
444        PatternType::DarkHorizontal => "darkHorizontal",
445        PatternType::DarkTrellis => "darkTrellis",
446        PatternType::DarkUp => "darkUp",
447        PatternType::DarkVertical => "darkVertical",
448        PatternType::Gray0625 => "gray0625",
449        PatternType::Gray125 => "gray125",
450        PatternType::LightDown => "lightDown",
451        PatternType::LightGray => "lightGray",
452        PatternType::LightGrid => "lightGrid",
453        PatternType::LightHorizontal => "lightHorizontal",
454        PatternType::LightTrellis => "lightTrellis",
455        PatternType::LightUp => "lightUp",
456        PatternType::LightVertical => "lightVertical",
457        PatternType::MediumGray => "mediumGray",
458    }
459}
460
461fn border_style_to_str(style: BorderStyle) -> &'static str {
462    match style {
463        BorderStyle::None => "none",
464        BorderStyle::Thin => "thin",
465        BorderStyle::Medium => "medium",
466        BorderStyle::Dashed => "dashed",
467        BorderStyle::Dotted => "dotted",
468        BorderStyle::Thick => "thick",
469        BorderStyle::Double => "double",
470        BorderStyle::Hair => "hair",
471        BorderStyle::MediumDashed => "mediumDashed",
472        BorderStyle::DashDot => "dashDot",
473        BorderStyle::MediumDashDot => "mediumDashDot",
474        BorderStyle::DashDotDot => "dashDotDot",
475        BorderStyle::MediumDashDotDot => "mediumDashDotDot",
476        BorderStyle::SlantDashDot => "slantDashDot",
477    }
478}
479
480fn horizontal_to_str(horizontal: HorizontalAlign) -> &'static str {
481    match horizontal {
482        HorizontalAlign::General => "general",
483        HorizontalAlign::Left => "left",
484        HorizontalAlign::Center => "center",
485        HorizontalAlign::Right => "right",
486        HorizontalAlign::Fill => "fill",
487        HorizontalAlign::Justify => "justify",
488        HorizontalAlign::CenterContinuous => "centerContinuous",
489        HorizontalAlign::Distributed => "distributed",
490    }
491}
492
493fn vertical_to_str(vertical: VerticalAlign) -> &'static str {
494    match vertical {
495        VerticalAlign::Top => "top",
496        VerticalAlign::Center => "center",
497        VerticalAlign::Bottom => "bottom",
498        VerticalAlign::Justify => "justify",
499        VerticalAlign::Distributed => "distributed",
500    }
501}