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::{ColumnType, DatabaseDocument, DbValue, Result};

use crate::common::{content_root_attrs, package_document, ExtraFile, MIME_ODB};

fn type_name(column_type: &ColumnType) -> &'static str {
    match column_type {
        ColumnType::Integer => "integer",
        ColumnType::Float => "float",
        ColumnType::Text => "string",
        ColumnType::Bool => "boolean",
    }
}

fn value_text(value: &DbValue) -> String {
    match value {
        DbValue::Null => String::new(),
        DbValue::Integer(v) => v.to_string(),
        DbValue::Float(v) => v.to_string(),
        DbValue::Text(v) => lo_core::escape_text(v),
        DbValue::Bool(v) => v.to_string(),
    }
}

pub fn serialize_database_document(database: &DatabaseDocument) -> 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:database", &[]);
    for table in &database.tables {
        xml.raw(&format!(
            "<table:table table:name=\"{}\">",
            lo_core::escape_attr(&table.name)
        ));
        xml.raw("<table:table-header-rows><table:table-row>");
        for column in &table.columns {
            xml.raw(&format!(
                "<table:table-cell office:value-type=\"string\"><text:p>{} ({})</text:p></table:table-cell>",
                lo_core::escape_text(&column.name),
                type_name(&column.column_type)
            ));
        }
        xml.raw("</table:table-row></table:table-header-rows>");
        for row in &table.rows {
            xml.raw("<table:table-row>");
            for value in row {
                xml.raw(&format!(
                    "<table:table-cell office:value-type=\"string\"><text:p>{}</text:p></table:table-cell>",
                    value_text(value)
                ));
            }
            xml.raw("</table:table-row>");
        }
        xml.raw("</table:table>");
    }
    xml.close();
    xml.close();
    xml.close();
    xml.finish()
}

fn csv_for_table(table: &lo_core::TableData) -> Vec<u8> {
    let mut out = String::new();
    out.push_str(
        &table
            .columns
            .iter()
            .map(|column| column.name.replace('"', "\"\""))
            .collect::<Vec<_>>()
            .join(","),
    );
    out.push('\n');
    for row in &table.rows {
        let cols: Vec<String> = row
            .iter()
            .map(|value| match value {
                DbValue::Null => String::new(),
                DbValue::Integer(v) => v.to_string(),
                DbValue::Float(v) => v.to_string(),
                DbValue::Text(v) => {
                    if v.contains(',') || v.contains('"') || v.contains('\n') {
                        format!("\"{}\"", v.replace('"', "\"\""))
                    } else {
                        v.clone()
                    }
                }
                DbValue::Bool(v) => v.to_string(),
            })
            .collect();
        out.push_str(&cols.join(","));
        out.push('\n');
    }
    out.into_bytes()
}

pub fn save_database_document(path: impl AsRef<Path>, database: &DatabaseDocument) -> Result<()> {
    let content = serialize_database_document(database);
    let mut extras = Vec::new();
    for table in &database.tables {
        extras.push(ExtraFile::new(
            format!("database/{}.csv", table.name),
            "text/csv",
            csv_for_table(table),
        ));
    }
    package_document(path, MIME_ODB, content, &database.meta, extras)
}