openprxl 0.1.0

A Rust spreadsheet library inspired by Python's openpyxl
Documentation
//! Serialization of `xl/styles.xml`.

use crate::error::Result;
use crate::style::{
    Alignment, Border, BorderStyle, CellXf, Fill, Font, NumberFormat, Protection, Side,
    StyleManager,
};
use crate::xml;
use quick_xml::events::{BytesEnd, BytesStart, Event};

pub fn write<W: std::io::Write>(sink: W, styles: &StyleManager) -> Result<()> {
    let mut writer = xml::writer(sink);

    let mut root = BytesStart::new("styleSheet");
    root.push_attribute(("xmlns", xml::NS_MAIN));
    writer.write_event(Event::Start(root))?;

    write_num_fmts(&mut writer, styles.number_formats())?;
    write_fonts(&mut writer, styles.fonts())?;
    write_fills(&mut writer, styles.fills())?;
    write_borders(&mut writer, styles.borders())?;
    write_cell_style_xfs(&mut writer)?;
    write_cell_styles(&mut writer)?;
    write_cell_xfs(&mut writer, styles.cell_xfs())?;

    writer.write_event(Event::End(BytesEnd::new("styleSheet")))?;
    Ok(())
}

fn write_num_fmts<W: std::io::Write>(
    writer: &mut quick_xml::Writer<W>,
    formats: &indexmap::IndexMap<NumberFormat, usize>,
) -> Result<()> {
    if formats.is_empty() {
        return Ok(());
    }
    let mut start = BytesStart::new("numFmts");
    start.push_attribute(("count", formats.len().to_string().as_str()));
    writer.write_event(Event::Start(start))?;

    for (fmt, id) in formats {
        let mut elem = BytesStart::new("numFmt");
        elem.push_attribute(("numFmtId", id.to_string().as_str()));
        elem.push_attribute(("formatCode", fmt.code.as_str()));
        writer.write_event(Event::Empty(elem))?;
    }

    writer.write_event(Event::End(BytesEnd::new("numFmts")))?;
    Ok(())
}

fn write_fonts<W: std::io::Write>(
    writer: &mut quick_xml::Writer<W>,
    fonts: &indexmap::IndexMap<Font, usize>,
) -> Result<()> {
    let count = fonts.len().max(1);
    let mut start = BytesStart::new("fonts");
    start.push_attribute(("count", count.to_string().as_str()));
    writer.write_event(Event::Start(start))?;

    if fonts.is_empty() {
        write_font_elem(writer, &Font::default())?;
    } else {
        let mut items: Vec<_> = fonts.iter().collect();
        items.sort_by_key(|(_, idx)| *idx);
        for (font, _) in items {
            write_font_elem(writer, font)?;
        }
    }

    writer.write_event(Event::End(BytesEnd::new("fonts")))?;
    Ok(())
}

fn write_font_elem<W: std::io::Write>(writer: &mut quick_xml::Writer<W>, font: &Font) -> Result<()> {
    writer.write_event(Event::Start(BytesStart::new("font")))?;
    if font.bold {
        writer.write_event(Event::Empty(BytesStart::new("b")))?;
    }
    if font.italic {
        writer.write_event(Event::Empty(BytesStart::new("i")))?;
    }
    if let Some(u) = font.underline {
        let mut elem = BytesStart::new("u");
        elem.push_attribute(("val", u.to_string().as_str()));
        writer.write_event(Event::Empty(elem))?;
    }
    if font.strike {
        writer.write_event(Event::Empty(BytesStart::new("strike")))?;
    }
    if let Some(size) = font.size {
        let mut elem = BytesStart::new("sz");
        elem.push_attribute(("val", size.to_string().as_str()));
        writer.write_event(Event::Empty(elem))?;
    }
    if let Some(color) = &font.color {
        writer.write_event(Event::Empty(color_elem("color", color)))?;
    }
    if let Some(name) = &font.name {
        let mut elem = BytesStart::new("name");
        elem.push_attribute(("val", name.as_str()));
        writer.write_event(Event::Empty(elem))?;
    }
    if let Some(family) = font.family {
        let mut elem = BytesStart::new("family");
        elem.push_attribute(("val", family.to_string().as_str()));
        writer.write_event(Event::Empty(elem))?;
    }
    if let Some(scheme) = &font.scheme {
        let mut elem = BytesStart::new("scheme");
        elem.push_attribute(("val", scheme.as_str()));
        writer.write_event(Event::Empty(elem))?;
    }
    writer.write_event(Event::End(BytesEnd::new("font")))?;
    Ok(())
}

fn write_fills<W: std::io::Write>(
    writer: &mut quick_xml::Writer<W>,
    fills: &indexmap::IndexMap<Fill, usize>,
) -> Result<()> {
    // Excel requires at least the two default fills at indices 0 and 1.
    let count = fills.len() + 2;
    let mut start = BytesStart::new("fills");
    start.push_attribute(("count", count.to_string().as_str()));
    writer.write_event(Event::Start(start))?;

    // Default none fill
    writer.write_event(Event::Start(BytesStart::new("fill")))?;
    let mut pt = BytesStart::new("patternFill");
    pt.push_attribute(("patternType", "none"));
    writer.write_event(Event::Empty(pt))?;
    writer.write_event(Event::End(BytesEnd::new("fill")))?;

    // Default gray125 fill
    writer.write_event(Event::Start(BytesStart::new("fill")))?;
    let mut pt = BytesStart::new("patternFill");
    pt.push_attribute(("patternType", "gray125"));
    writer.write_event(Event::Empty(pt))?;
    writer.write_event(Event::End(BytesEnd::new("fill")))?;

    let mut items: Vec<_> = fills.iter().collect();
    items.sort_by_key(|(_, idx)| *idx);
    for (fill, _) in items {
        write_fill_elem(writer, fill)?;
    }

    writer.write_event(Event::End(BytesEnd::new("fills")))?;
    Ok(())
}

fn write_fill_elem<W: std::io::Write>(writer: &mut quick_xml::Writer<W>, fill: &Fill) -> Result<()> {
    writer.write_event(Event::Start(BytesStart::new("fill")))?;
    match fill {
        Fill::None => {
            let mut pt = BytesStart::new("patternFill");
            pt.push_attribute(("patternType", "none"));
            writer.write_event(Event::Empty(pt))?;
        }
        Fill::Pattern(p) => {
            let mut pt = BytesStart::new("patternFill");
            pt.push_attribute(("patternType", p.pattern_type.to_string().as_str()));
            writer.write_event(Event::Start(pt))?;
            if let Some(fg) = &p.fg_color {
                writer.write_event(Event::Empty(color_elem("fgColor", fg)))?;
            }
            if let Some(bg) = &p.bg_color {
                writer.write_event(Event::Empty(color_elem("bgColor", bg)))?;
            }
            writer.write_event(Event::End(BytesEnd::new("patternFill")))?;
        }
    }
    writer.write_event(Event::End(BytesEnd::new("fill")))?;
    Ok(())
}

fn write_borders<W: std::io::Write>(
    writer: &mut quick_xml::Writer<W>,
    borders: &indexmap::IndexMap<Border, usize>,
) -> Result<()> {
    let count = borders.len().max(1);
    let mut start = BytesStart::new("borders");
    start.push_attribute(("count", count.to_string().as_str()));
    writer.write_event(Event::Start(start))?;

    if borders.is_empty() {
        write_border_elem(writer, &Border::default())?;
    } else {
        let mut items: Vec<_> = borders.iter().collect();
        items.sort_by_key(|(_, idx)| *idx);
        for (border, _) in items {
            write_border_elem(writer, border)?;
        }
    }

    writer.write_event(Event::End(BytesEnd::new("borders")))?;
    Ok(())
}

fn write_border_elem<W: std::io::Write>(writer: &mut quick_xml::Writer<W>, border: &Border) -> Result<()> {
    writer.write_event(Event::Start(BytesStart::new("border")))?;
    write_side(writer, "left", border.left.clone())?;
    write_side(writer, "right", border.right.clone())?;
    write_side(writer, "top", border.top.clone())?;
    write_side(writer, "bottom", border.bottom.clone())?;
    write_side(
        writer,
        "diagonal",
        border.diagonal.as_ref().map(|d| Side {
            style: d.style,
            color: d.color.clone(),
        }),
    )?;
    writer.write_event(Event::End(BytesEnd::new("border")))?;
    Ok(())
}

fn write_side<W: std::io::Write>(
    writer: &mut quick_xml::Writer<W>,
    name: &str,
    side: Option<Side>,
) -> Result<()> {
    let mut elem = BytesStart::new(name);
    match side {
        Some(s) if s.style != BorderStyle::None => {
            elem.push_attribute(("style", s.style.to_string().as_str()));
            writer.write_event(Event::Start(elem))?;
            if let Some(color) = &s.color {
                writer.write_event(Event::Empty(color_elem("color", color)))?;
            }
            writer.write_event(Event::End(BytesEnd::new(name)))?;
        }
        _ => {
            writer.write_event(Event::Empty(elem))?;
        }
    }
    Ok(())
}

fn write_cell_style_xfs<W: std::io::Write>(writer: &mut quick_xml::Writer<W>) -> Result<()> {
    let mut start = BytesStart::new("cellStyleXfs");
    start.push_attribute(("count", "1"));
    writer.write_event(Event::Start(start))?;

    let mut xf = BytesStart::new("xf");
    xf.push_attribute(("numFmtId", "0"));
    xf.push_attribute(("fontId", "0"));
    xf.push_attribute(("fillId", "0"));
    xf.push_attribute(("borderId", "0"));
    writer.write_event(Event::Empty(xf))?;

    writer.write_event(Event::End(BytesEnd::new("cellStyleXfs")))?;
    Ok(())
}

fn write_cell_styles<W: std::io::Write>(writer: &mut quick_xml::Writer<W>) -> Result<()> {
    let mut start = BytesStart::new("cellStyles");
    start.push_attribute(("count", "1"));
    writer.write_event(Event::Start(start))?;

    let mut style = BytesStart::new("cellStyle");
    style.push_attribute(("name", "Normal"));
    style.push_attribute(("xfId", "0"));
    style.push_attribute(("builtinId", "0"));
    writer.write_event(Event::Empty(style))?;

    writer.write_event(Event::End(BytesEnd::new("cellStyles")))?;
    Ok(())
}

fn write_cell_xfs<W: std::io::Write>(writer: &mut quick_xml::Writer<W>, xfs: &[CellXf]) -> Result<()> {
    let count = xfs.len().max(1);
    let mut start = BytesStart::new("cellXfs");
    start.push_attribute(("count", count.to_string().as_str()));
    writer.write_event(Event::Start(start))?;

    if xfs.is_empty() {
        let mut xf = BytesStart::new("xf");
        xf.push_attribute(("numFmtId", "0"));
        xf.push_attribute(("fontId", "0"));
        xf.push_attribute(("fillId", "0"));
        xf.push_attribute(("borderId", "0"));
        xf.push_attribute(("xfId", "0"));
        writer.write_event(Event::Empty(xf))?;
    } else {
        for xf in xfs {
            write_cell_xf(writer, xf)?;
        }
    }

    writer.write_event(Event::End(BytesEnd::new("cellXfs")))?;
    Ok(())
}

fn write_cell_xf<W: std::io::Write>(writer: &mut quick_xml::Writer<W>, xf: &CellXf) -> Result<()> {
    let mut start = BytesStart::new("xf");
    start.push_attribute(("numFmtId", xf.num_fmt_id.unwrap_or(0).to_string().as_str()));
    start.push_attribute(("fontId", xf.font_id.unwrap_or(0).to_string().as_str()));
    // fillId offset by 2 because of the two default fills
    start.push_attribute(
        (
            "fillId",
            xf.fill_id.map(|i| i + 2).unwrap_or(0).to_string().as_str(),
        ),
    );
    start.push_attribute(("borderId", xf.border_id.unwrap_or(0).to_string().as_str()));
    start.push_attribute(("xfId", "0"));

    let has_alignment = xf.alignment.is_some();
    let has_protection = xf.protection.is_some();

    if has_alignment || has_protection {
        writer.write_event(Event::Start(start))?;
        if let Some(a) = &xf.alignment {
            write_alignment(writer, a)?;
        }
        if let Some(p) = &xf.protection {
            write_protection(writer, p)?;
        }
        writer.write_event(Event::End(BytesEnd::new("xf")))?;
    } else {
        writer.write_event(Event::Empty(start))?;
    }

    Ok(())
}

fn write_alignment<W: std::io::Write>(writer: &mut quick_xml::Writer<W>, a: &Alignment) -> Result<()> {
    let mut elem = BytesStart::new("alignment");
    if let Some(h) = a.horizontal {
        elem.push_attribute(("horizontal", h.to_string().as_str()));
    }
    if let Some(v) = a.vertical {
        elem.push_attribute(("vertical", v.to_string().as_str()));
    }
    if a.wrap_text {
        elem.push_attribute(("wrapText", "1"));
    }
    if a.shrink_to_fit {
        elem.push_attribute(("shrinkToFit", "1"));
    }
    if let Some(rot) = a.text_rotation {
        elem.push_attribute(("textRotation", rot.to_string().as_str()));
    }
    writer.write_event(Event::Empty(elem))?;
    Ok(())
}

fn write_protection<W: std::io::Write>(writer: &mut quick_xml::Writer<W>, p: &Protection) -> Result<()> {
    let mut elem = BytesStart::new("protection");
    elem.push_attribute(("locked", if p.locked { "1" } else { "0" }));
    elem.push_attribute(("hidden", if p.hidden { "1" } else { "0" }));
    writer.write_event(Event::Empty(elem))?;
    Ok(())
}

fn color_elem<'a>(name: &'a str, rgb: &'a str) -> BytesStart<'a> {
    let mut elem = BytesStart::new(name);
    elem.push_attribute(("rgb", rgb));
    elem
}