lo_odf 0.1.0

ODF package serializers for text, spreadsheet, presentation, drawing, formula, and database documents
Documentation
use std::path::Path;

use lo_core::{CellAddr, CellValue, Result, Workbook};

use crate::common::{content_root_attrs, package_document, MIME_ODS};

fn odf_formula(formula: &str) -> String {
    if formula.starts_with("of:=") {
        formula.to_string()
    } else if formula.starts_with('=') {
        format!("of:{formula}")
    } else {
        format!("of:={formula}")
    }
}

fn cell_xml(value: &CellValue) -> String {
    match value {
        CellValue::Empty => "<table:table-cell/>".to_string(),
        CellValue::Number(number) => format!(
            "<table:table-cell office:value-type=\"float\" office:value=\"{number}\"><text:p>{number}</text:p></table:table-cell>"
        ),
        CellValue::Text(text) => format!(
            "<table:table-cell office:value-type=\"string\"><text:p>{}</text:p></table:table-cell>",
            lo_core::escape_text(text)
        ),
        CellValue::Bool(value) => format!(
            "<table:table-cell office:value-type=\"boolean\" office:boolean-value=\"{}\"><text:p>{}</text:p></table:table-cell>",
            value,
            value
        ),
        CellValue::Formula(formula) => format!(
            "<table:table-cell table:formula=\"{}\" office:value-type=\"string\"><text:p>{}</text:p></table:table-cell>",
            lo_core::escape_attr(&odf_formula(formula)),
            lo_core::escape_text(formula)
        ),
        CellValue::Error(message) => format!(
            "<table:table-cell office:value-type=\"string\"><text:p>#ERR {}</text:p></table:table-cell>",
            lo_core::escape_text(message)
        ),
    }
}

pub fn serialize_spreadsheet_document(book: &Workbook) -> String {
    let mut xml = lo_core::XmlBuilder::new();
    xml.declaration();
    xml.open("office:document-content", &content_root_attrs());
    xml.empty("office:scripts", &[]);
    xml.empty("office:automatic-styles", &[]);
    xml.open("office:body", &[]);
    xml.open("office:spreadsheet", &[]);
    for sheet in &book.sheets {
        xml.raw(&format!(
            "<table:table table:name=\"{}\">",
            lo_core::escape_attr(&sheet.name)
        ));
        let (max_row, max_col) = sheet.max_extent();
        for row in 0..=max_row {
            xml.raw("<table:table-row>");
            for col in 0..=max_col {
                let addr = CellAddr::new(row, col);
                let cell = sheet.get(addr).map(|cell| &cell.value).unwrap_or(&CellValue::Empty);
                xml.raw(&cell_xml(cell));
            }
            xml.raw("</table:table-row>");
        }
        xml.raw("</table:table>");
    }
    xml.close();
    xml.close();
    xml.close();
    xml.finish()
}

pub fn save_spreadsheet_document(path: impl AsRef<Path>, book: &Workbook) -> Result<()> {
    let content = serialize_spreadsheet_document(book);
    package_document(path, MIME_ODS, content, &book.meta, Vec::new())
}