openprxl 0.1.0

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

use crate::cell::Value;
use crate::error::Result;
use crate::reference::CellRef;
use crate::shared_strings::SharedStrings;
use crate::workbook::Workbook;
use crate::worksheet::Worksheet;
use crate::xml;
use quick_xml::events::{BytesEnd, BytesStart, Event};

pub fn write<W: std::io::Write>(
    sink: W,
    wb: &Workbook,
    ws: &Worksheet,
    strings: &mut SharedStrings,
) -> Result<()> {
    let mut writer = xml::writer(sink);

    let mut root = BytesStart::new("worksheet");
    root.push_attribute(("xmlns", xml::NS_MAIN));
    root.push_attribute(("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"));
    writer.write_event(Event::Start(root))?;

    write_sheet_views(&mut writer, ws)?;
    write_sheet_format_pr(&mut writer)?;
    write_cols(&mut writer, ws)?;
    write_sheet_data(&mut writer, wb, ws, strings)?;
    write_merged_cells(&mut writer, ws)?;
    write_page_margins(&mut writer)?;

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

fn write_sheet_views<W: std::io::Write>(writer: &mut quick_xml::Writer<W>, _ws: &Worksheet) -> Result<()> {
    let views = BytesStart::new("sheetViews");
    writer.write_event(Event::Start(views))?;
    writer.write_event(Event::Empty(BytesStart::new("sheetView")))?;
    writer.write_event(Event::End(BytesEnd::new("sheetViews")))?;
    Ok(())
}

fn write_sheet_format_pr<W: std::io::Write>(writer: &mut quick_xml::Writer<W>) -> Result<()> {
    let mut elem = BytesStart::new("sheetFormatPr");
    elem.push_attribute(("defaultRowHeight", "15"));
    writer.write_event(Event::Empty(elem))?;
    Ok(())
}

fn write_cols<W: std::io::Write>(writer: &mut quick_xml::Writer<W>, ws: &Worksheet) -> Result<()> {
    if ws.col_widths.is_empty() && ws.hidden_cols.is_empty() {
        return Ok(());
    }

    let mut all_cols: std::collections::BTreeSet<u32> = ws.col_widths.keys().copied().collect();
    all_cols.extend(ws.hidden_cols.iter().copied());

    writer.write_event(Event::Start(BytesStart::new("cols")))?;
    for col in all_cols {
        let mut elem = BytesStart::new("col");
        elem.push_attribute(("min", col.to_string().as_str()));
        elem.push_attribute(("max", col.to_string().as_str()));
        if let Some(width) = ws.col_widths.get(&col) {
            elem.push_attribute(("width", width.to_string().as_str()));
            elem.push_attribute(("customWidth", "1"));
        }
        if ws.hidden_cols.contains(&col) {
            elem.push_attribute(("hidden", "1"));
        }
        writer.write_event(Event::Empty(elem))?;
    }
    writer.write_event(Event::End(BytesEnd::new("cols")))?;
    Ok(())
}

fn write_sheet_data<W: std::io::Write>(
    writer: &mut quick_xml::Writer<W>,
    wb: &Workbook,
    ws: &Worksheet,
    strings: &mut SharedStrings,
) -> Result<()> {
    writer.write_event(Event::Start(BytesStart::new("sheetData")))?;

    // Group cells by row
    let mut rows: std::collections::BTreeMap<u32, Vec<(u32, &crate::cell::Cell)>> = std::collections::BTreeMap::new();
    for ((row, col), cell) in &ws.cells {
        rows.entry(*row).or_default().push((*col, cell));
    }

    for (row_idx, cols) in rows {
        let mut row_start = BytesStart::new("row");
        row_start.push_attribute(("r", row_idx.to_string().as_str()));
        if let Some(ht) = ws.row_heights.get(&row_idx) {
            row_start.push_attribute(("ht", ht.to_string().as_str()));
            row_start.push_attribute(("customHeight", "1"));
        }
        if ws.hidden_rows.contains(&row_idx) {
            row_start.push_attribute(("hidden", "1"));
        }
        writer.write_event(Event::Start(row_start))?;

        for (col_idx, cell) in cols {
            write_cell(writer, wb, ws, row_idx, col_idx, cell, strings)?;
        }

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

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

fn write_cell<W: std::io::Write>(
    writer: &mut quick_xml::Writer<W>,
    _wb: &Workbook,
    _ws: &Worksheet,
    row: u32,
    col: u32,
    cell: &crate::cell::Cell,
    strings: &mut SharedStrings,
) -> Result<()> {
    // Skip truly empty cells with no style.
    if cell.value.is_empty() && cell.style_id.is_none() {
        return Ok(());
    }

    let mut elem = BytesStart::new("c");
    let reference = CellRef::new(row, col).map(|r| r.to_a1()).unwrap_or_default();
    elem.push_attribute(("r", reference.as_str()));

    if let Some(style_id) = cell.style_id {
        elem.push_attribute(("s", style_id.index().to_string().as_str()));
    }

    match &cell.value {
        Value::Empty => {
            writer.write_event(Event::Empty(elem))?;
        }
        Value::Number(n) => {
            elem.push_attribute(("t", "n"));
            writer.write_event(Event::Start(elem))?;
            writer.write_event(Event::Start(BytesStart::new("v")))?;
            xml::write_text(writer, &n.to_string())?;
            writer.write_event(Event::End(BytesEnd::new("v")))?;
            writer.write_event(Event::End(BytesEnd::new("c")))?;
        }
        Value::String(s) => {
            let idx = strings.add(s);
            elem.push_attribute(("t", "s"));
            writer.write_event(Event::Start(elem))?;
            writer.write_event(Event::Start(BytesStart::new("v")))?;
            xml::write_text(writer, &idx.to_string())?;
            writer.write_event(Event::End(BytesEnd::new("v")))?;
            writer.write_event(Event::End(BytesEnd::new("c")))?;
        }
        Value::Bool(b) => {
            elem.push_attribute(("t", "b"));
            writer.write_event(Event::Start(elem))?;
            writer.write_event(Event::Start(BytesStart::new("v")))?;
            xml::write_text(writer, if *b { "1" } else { "0" })?;
            writer.write_event(Event::End(BytesEnd::new("v")))?;
            writer.write_event(Event::End(BytesEnd::new("c")))?;
        }
        Value::Formula(f) => {
            let formula_text = f.strip_prefix('=').unwrap_or(f.as_str());
            writer.write_event(Event::Start(elem))?;
            writer.write_event(Event::Start(BytesStart::new("f")))?;
            xml::write_text(writer, formula_text)?;
            writer.write_event(Event::End(BytesEnd::new("f")))?;
            writer.write_event(Event::End(BytesEnd::new("c")))?;
        }
        Value::Error(e) => {
            elem.push_attribute(("t", "e"));
            writer.write_event(Event::Start(elem))?;
            writer.write_event(Event::Start(BytesStart::new("v")))?;
            xml::write_text(writer, e)?;
            writer.write_event(Event::End(BytesEnd::new("v")))?;
            writer.write_event(Event::End(BytesEnd::new("c")))?;
        }
        Value::Date(d) => {
            elem.push_attribute(("t", "n"));
            writer.write_event(Event::Start(elem))?;
            writer.write_event(Event::Start(BytesStart::new("v")))?;
            xml::write_text(writer, &d.to_string())?;
            writer.write_event(Event::End(BytesEnd::new("v")))?;
            writer.write_event(Event::End(BytesEnd::new("c")))?;
        }
    }

    Ok(())
}

fn write_merged_cells<W: std::io::Write>(writer: &mut quick_xml::Writer<W>, ws: &Worksheet) -> Result<()> {
    if ws.merged_cells.is_empty() {
        return Ok(());
    }
    let mut start = BytesStart::new("mergeCells");
    start.push_attribute(("count", ws.merged_cells.len().to_string().as_str()));
    writer.write_event(Event::Start(start))?;

    for range in &ws.merged_cells {
        let mut elem = BytesStart::new("mergeCell");
        elem.push_attribute(("ref", range.to_a1().as_str()));
        writer.write_event(Event::Empty(elem))?;
    }

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

fn write_page_margins<W: std::io::Write>(writer: &mut quick_xml::Writer<W>) -> Result<()> {
    let mut elem = BytesStart::new("pageMargins");
    elem.push_attribute(("left", "0.7"));
    elem.push_attribute(("right", "0.7"));
    elem.push_attribute(("top", "0.75"));
    elem.push_attribute(("bottom", "0.75"));
    elem.push_attribute(("header", "0.3"));
    elem.push_attribute(("footer", "0.3"));
    writer.write_event(Event::Empty(elem))?;
    Ok(())
}