package ods import ( "archive/zip" "bytes" "encoding/xml" "fmt" "strconv" "strings" "time" ) // Write produces an ODS file as []byte from a Workbook. func Write(wb *Workbook) ([]byte, error) { var buf bytes.Buffer zw := zip.NewWriter(&buf) // mimetype MUST be first entry, stored (not compressed) mimeHeader := &zip.FileHeader{ Name: "mimetype", Method: zip.Store, } mw, err := zw.CreateHeader(mimeHeader) if err != nil { return nil, fmt.Errorf("create mimetype: %w", err) } if _, err := mw.Write([]byte("application/vnd.oasis.opendocument.spreadsheet")); err != nil { return nil, fmt.Errorf("write mimetype: %w", err) } // META-INF/manifest.xml if err := writeManifest(zw); err != nil { return nil, err } // meta.xml if err := writeMeta(zw, wb.Meta); err != nil { return nil, err } // styles.xml if err := writeStyles(zw); err != nil { return nil, err } // content.xml if err := writeContent(zw, wb.Sheets); err != nil { return nil, err } if err := zw.Close(); err != nil { return nil, fmt.Errorf("close zip: %w", err) } return buf.Bytes(), nil } // writeManifest writes META-INF/manifest.xml. func writeManifest(zw *zip.Writer) error { w, err := zw.Create("META-INF/manifest.xml") if err != nil { return fmt.Errorf("create manifest: %w", err) } const manifest = xml.Header + ` ` _, err = w.Write([]byte(manifest)) return err } // writeMeta writes meta.xml with custom properties. func writeMeta(zw *zip.Writer, meta map[string]string) error { w, err := zw.Create("meta.xml") if err != nil { return fmt.Errorf("create meta.xml: %w", err) } var b strings.Builder b.WriteString(xml.Header) b.WriteString(``) b.WriteString(``) b.WriteString(`Silo`) b.WriteString(fmt.Sprintf(`%s`, time.Now().UTC().Format(time.RFC3339))) if len(meta) > 0 { b.WriteString(``) // Encode all meta as key=value pairs separated by newlines var pairs []string for k, v := range meta { pairs = append(pairs, xmlEscape(k)+"="+xmlEscape(v)) } b.WriteString(xmlEscape(strings.Join(pairs, "\n"))) b.WriteString(``) } b.WriteString(``) b.WriteString(``) _, err = w.Write([]byte(b.String())) return err } // writeStyles writes styles.xml with header, currency, and hidden column styles. func writeStyles(zw *zip.Writer) error { w, err := zw.Create("styles.xml") if err != nil { return fmt.Errorf("create styles.xml: %w", err) } const styles = xml.Header + ` ` _, err = w.Write([]byte(styles)) return err } // writeContent writes content.xml containing all sheet data. func writeContent(zw *zip.Writer, sheets []Sheet) error { w, err := zw.Create("content.xml") if err != nil { return fmt.Errorf("create content.xml: %w", err) } var b strings.Builder b.WriteString(xml.Header) b.WriteString(``) // Automatic styles (defined in content.xml for cell/column styles) b.WriteString(``) // Currency data style b.WriteString(``) b.WriteString(`$`) b.WriteString(``) b.WriteString(``) // Header cell style (bold) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) // Currency cell style b.WriteString(``) b.WriteString(``) // Row status colors writeColorStyle(&b, "ce-synced", "#c6efce") writeColorStyle(&b, "ce-modified", "#ffeb9c") writeColorStyle(&b, "ce-new", "#bdd7ee") writeColorStyle(&b, "ce-error", "#ffc7ce") writeColorStyle(&b, "ce-conflict", "#f4b084") // Column styles b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) // Body b.WriteString(``) b.WriteString(``) for _, sheet := range sheets { writeSheet(&b, &sheet) } b.WriteString(``) b.WriteString(``) b.WriteString(``) _, err = w.Write([]byte(b.String())) return err } func writeColorStyle(b *strings.Builder, name, color string) { b.WriteString(fmt.Sprintf(``, name)) b.WriteString(fmt.Sprintf(``, color)) b.WriteString(``) } func writeSheet(b *strings.Builder, sheet *Sheet) { b.WriteString(fmt.Sprintf(``, xmlEscape(sheet.Name))) // Column definitions if len(sheet.Columns) > 0 { for _, col := range sheet.Columns { styleName := "co-default" if col.Width != "" { styleName = "co-wide" } if col.Hidden { b.WriteString(fmt.Sprintf(``, styleName)) } else { b.WriteString(fmt.Sprintf(``, styleName)) } } } // Rows for _, row := range sheet.Rows { if row.IsBlank { b.WriteString(``) b.WriteString(``) b.WriteString(``) continue } b.WriteString(``) for _, cell := range row.Cells { writeCell(b, &cell) } b.WriteString(``) } b.WriteString(``) } func writeCell(b *strings.Builder, cell *Cell) { switch cell.Type { case CellEmpty: b.WriteString(``) case CellFormula: // Formula cells: the formula attribute uses the of: namespace b.WriteString(fmt.Sprintf(``, xmlEscape(cell.Formula))) b.WriteString(``) case CellFloat: b.WriteString(fmt.Sprintf(``, xmlEscape(cell.Value))) b.WriteString(fmt.Sprintf(`%s`, xmlEscape(cell.Value))) b.WriteString(``) case CellCurrency: b.WriteString(fmt.Sprintf(``, xmlEscape(cell.Value))) b.WriteString(fmt.Sprintf(`$%s`, xmlEscape(cell.Value))) b.WriteString(``) case CellDate: b.WriteString(fmt.Sprintf(``, xmlEscape(cell.Value))) b.WriteString(fmt.Sprintf(`%s`, xmlEscape(cell.Value))) b.WriteString(``) default: // CellString b.WriteString(``) b.WriteString(fmt.Sprintf(`%s`, xmlEscape(cell.Value))) b.WriteString(``) } } // xmlEscape escapes special XML characters. func xmlEscape(s string) string { var b strings.Builder if err := xml.EscapeText(&b, []byte(s)); err != nil { return s } return b.String() } // Helper functions for building cells // StringCell creates a string cell. func StringCell(value string) Cell { return Cell{Value: value, Type: CellString} } // FloatCell creates a float cell. func FloatCell(value float64) Cell { return Cell{Value: strconv.FormatFloat(value, 'f', -1, 64), Type: CellFloat} } // CurrencyCell creates a currency (USD) cell. func CurrencyCell(value float64) Cell { return Cell{Value: fmt.Sprintf("%.2f", value), Type: CellCurrency} } // FormulaCell creates a formula cell. func FormulaCell(formula string) Cell { return Cell{Formula: formula, Type: CellFormula} } // EmptyCell creates an empty cell. func EmptyCell() Cell { return Cell{Type: CellEmpty} } // IntCell creates an integer cell stored as float. func IntCell(value int) Cell { return Cell{Value: strconv.Itoa(value), Type: CellFloat} } // HeaderCell creates a string cell intended for header rows. // The header style is applied at the row level or by the caller. func HeaderCell(value string) Cell { return Cell{Value: value, Type: CellString} }