The binary no longer links excelize (nor efp, nfp, mscfb, msoleps, go-deepcopy and x/net); tests still use it to check what 012 writes and as the differential test's reference.
- commit
- e5c66aafc38a45449b9e3d5fe71e351514a4793f
- parent
- 627e90e5178c9b8dacc234d94d06c706e498f058
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 226 | ++++++++++------------ |
| M | internal/ | 63 | ++++++ |
| A | internal/ | 316 | ++++++++++++++++++++++++++++++ |
@@ -1,16 +1,21 @@
11 package fileio22 33 import (4+ "archive/zip"5+ "bufio"46 "context"57 "fmt"68 "io"9+ "strconv"710 "strings"811 9- "github.com/xuri/excelize/v2"10-1112 "github.com/FelineStateMachine/012/internal/sheet"1213 )1314 15+// XLSX files are read by xlsximport.go (on xlsxpkg.go, xlsxbook.go,16+// xlsxstyles.go and xlsxsheet.go) and written here and in xlsxwrite.go,17+// all with archive/zip and encoding/xml's escaping.18+1419 // Column widths: Excel measures in characters of its default font, 0121520 // in terminal columns including one of padding.1621 const (@@ -29,159 +34,132 @@ type xlsxStyle struct {
2934 // exportXLSX writes a workbook: the snapshot's sheets (or just the3035 // snapshot), each with values, formulas in Excel's syntax with their3136 // results cached, number formats, text styles, alignment and column32-// widths, and the named ranges.37+// widths, and the named ranges. Excel recalculates it on opening.3338 func exportXLSX(_ context.Context, name string, snap *Snapshot, _ ExportOptions) (*ExportResult, error) {34- x := excelize.NewFile()35- defer x.Close()3639 sheets := snap.Sheets3740 if len(sheets) == 0 {3841 sheets = []*Snapshot{snap}3942 }40- w := &xlsxWriter{x: x, styleIDs: map[xlsxStyle]int{}, multi: len(sheets) > 1}43+ w := &xlsxWriter{styles: newXLSXStyleTable(), multi: len(sheets) > 1}4144 res := &ExportResult{}45+ names, active := make([]string, len(sheets)), 046+ used := map[string]bool{}4247 for i, sn := range sheets {43- ws := sheetName(sn.Name)44- var err error45- if i == 0 {46- err = x.SetSheetName("Sheet1", ws)47- } else {48- _, err = x.NewSheet(ws)48+ names[i] = uniqueSheetName(sheetName(sn.Name), used)49+ if sn == snap {50+ active = i4951 }50- if err != nil {51- return nil, err52+ }53+ err := writeFile(name, func(out io.Writer) error {54+ zw := zip.NewWriter(out)55+ for i, sn := range sheets {56+ rows, err := w.writeSheet(zw, i, names[i], sn, i == active)57+ if err != nil {58+ return err59+ }60+ res.Rows += rows5261 }53- rows, err := w.sheet(ws, sn)54- if err != nil {55- return nil, err62+ if err := writePart(zw, "xl/styles.xml", w.styles.xml()); err != nil {63+ return err5664 }57- res.Rows += rows58- if sn == snap || snap.Sheets == nil {59- x.SetActiveSheet(i)65+ if err := writePackage(zw, names, active, snap.Names); err != nil {66+ return err6067 }61- }62- for _, n := range snap.Names {63- if err := x.SetDefinedName(&excelize.DefinedName{Name: n[0], RefersTo: n[1]}); err != nil {64- return nil, err65- }66- }67- yes := true68- if err := x.SetCalcProps(&excelize.CalcPropsOptions{FullCalcOnLoad: &yes}); err != nil {68+ return zw.Close()69+ })70+ if err != nil {6971 return nil, err7072 }7173 if w.values > 0 {7274 res.Notes = append(res.Notes, fmt.Sprintf("%s with no Excel equivalent saved as values, e.g. %s",7375 count(w.values, "formula", "formulas"), w.example))7476 }75- return res, writeFile(name, func(w io.Writer) error { return x.Write(w) })77+ return res, nil7678 }7779 78-// xlsxWriter writes sheets into a workbook, sharing cell styles.79-type xlsxWriter struct {80- x *excelize.File81- styleIDs map[xlsxStyle]int82- values int // formulas written as values83- example string // the first of them84- multi bool // examples name their sheet80+// uniqueSheetName is name, or name with a number when another sheet81+// already took it (Excel's names ignore case).82+func uniqueSheetName(name string, used map[string]bool) string {83+ out := name84+ for n := 2; used[strings.ToLower(out)]; n++ {85+ suffix := " (" + strconv.Itoa(n) + ")"86+ r := []rune(name)87+ out = string(r[:min(len(r), 31-len(suffix))]) + suffix88+ }89+ used[strings.ToLower(out)] = true90+ return out8591 }8692 87-func (w *xlsxWriter) styleFor(f sheet.Format, st sheet.Style) (int, error) {88- key := xlsxStyle{f, st}89- if id, ok := w.styleIDs[key]; ok {90- return id, nil91- }92- xs := &excelize.Style{}93- if code := excelCode(f); code != "" {94- xs.CustomNumFmt = &code95- }96- if st.Bold || st.Italic || st.Underline || st.Strikethrough {97- xs.Font = &excelize.Font{Bold: st.Bold, Italic: st.Italic, Strike: st.Strikethrough}98- if st.Underline {99- xs.Font.Underline = "single"100- }101- }102- if st.Align != sheet.AlignAuto {103- xs.Alignment = &excelize.Alignment{Horizontal: st.Align.String()}104- }105- id, err := w.x.NewStyle(xs)106- w.styleIDs[key] = id107- return id, err108-}93+const (94+ xmlHead = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` + "\n"95+ relsNS = `http://schemas.openxmlformats.org/package/2006/relationships`96+ officeRel = `http://schemas.openxmlformats.org/officeDocument/2006/relationships`97+ sheetMain = `http://schemas.openxmlformats.org/spreadsheetml/2006/main`98+ mlType = `application/vnd.openxmlformats-officedocument.spreadsheetml.`99+)109100 110-// sheet writes a snapshot to sheet ws and returns the rows written.111-func (w *xlsxWriter) sheet(ws string, snap *Snapshot) (int, error) {112- sw, err := w.x.NewStreamWriter(ws)113- if err != nil {114- return 0, err115- }116- for c, width := range snap.Widths {117- if err := sw.SetColWidth(c+1, c+1, float64(max(width-excelPadding, 1))); err != nil {118- return 0, err119- }120- }121- r := snap.Range122- line := make([]any, r.To.Col-r.From.Col+1)123- for row := r.From.Row; row <= r.To.Row; row++ {124- for col := r.From.Col; col <= r.To.Col; col++ {125- a := sheet.Addr{Col: col, Row: row}126- line[col-r.From.Col] = nil127- if c, ok := snap.Cells[a]; ok {128- if line[col-r.From.Col], err = w.cell(ws, a, c); err != nil {129- return 0, err130- }131- }101+// writePackage writes the parts around the worksheets and styles: the102+// workbook with its sheets, active tab, names and calculation settings,103+// the relationships and the content types.104+func writePackage(zw *zip.Writer, names []string, active int, defined [][2]string) error {105+ var types, rels, book strings.Builder106+ types.WriteString(xmlHead + `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +107+ `<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +108+ `<Default Extension="xml" ContentType="application/xml"/>` +109+ `<Override PartName="/xl/workbook.xml" ContentType="` + mlType + `sheet.main+xml"/>` +110+ `<Override PartName="/xl/styles.xml" ContentType="` + mlType + `styles+xml"/>`)111+ rels.WriteString(xmlHead + `<Relationships xmlns="` + relsNS + `">`)112+ book.WriteString(xmlHead + `<workbook xmlns="` + sheetMain + `" xmlns:r="` + officeRel + `">` +113+ `<bookViews><workbookView activeTab="` + strconv.Itoa(active) + `"/></bookViews><sheets>`)114+ for i, ws := range names {115+ n := strconv.Itoa(i + 1)116+ fmt.Fprintf(&types, `<Override PartName="/xl/worksheets/sheet%s.xml" ContentType="%sworksheet+xml"/>`, n, mlType)117+ fmt.Fprintf(&rels, `<Relationship Id="rId%s" Type="%s/worksheet" Target="worksheets/sheet%s.xml"/>`, n, officeRel, n)118+ fmt.Fprintf(&book, `<sheet name="%s" sheetId="%s" r:id="rId%s"/>`, escapeXML(ws, true), n, n)119+ }120+ fmt.Fprintf(&rels, `<Relationship Id="rId%d" Type="%s/styles" Target="styles.xml"/></Relationships>`, len(names)+1, officeRel)121+ types.WriteString(`</Types>`)122+ book.WriteString(`</sheets>`)123+ if len(defined) > 0 {124+ book.WriteString(`<definedNames>`)125+ for _, n := range defined {126+ fmt.Fprintf(&book, `<definedName name="%s">%s</definedName>`, escapeXML(n[0], true), escapeXML(n[1], false))132127 }133- cell, _ := excelize.CoordinatesToCellName(r.From.Col+1, row+1)134- if err := sw.SetRow(cell, line); err != nil {135- return 0, err128+ book.WriteString(`</definedNames>`)129+ }130+ book.WriteString(`<calcPr calcId="191029" fullCalcOnLoad="1"/></workbook>`)131+ for _, p := range []struct{ name, body string }{132+ {"[Content_Types].xml", types.String()},133+ {"_rels/.rels", xmlHead + `<Relationships xmlns="` + relsNS + `"><Relationship Id="rId1" Type="` + officeRel + `/officeDocument" Target="xl/workbook.xml"/></Relationships>`},134+ {"xl/workbook.xml", book.String()},135+ {"xl/_rels/workbook.xml.rels", rels.String()},136+ } {137+ if err := writePart(zw, p.name, p.body); err != nil {138+ return err136139 }137140 }138- if err := sw.Flush(); err != nil {139- return 0, err140- }141- return r.To.Row - r.From.Row + 1, nil141+ return nil142142 }143143 144-// cell is the snapshot cell c at a as excelize writes it. A formula with145-// no Excel equivalent is written as its value, and counted.146-func (w *xlsxWriter) cell(ws string, a sheet.Addr, c SnapCell) (excelize.Cell, error) {147- // A formula's inferred format is written too, so its result shows148- // the same in Excel.149- id := 0150- if !c.Format.IsZero() || !c.Style.IsZero() {151- var err error152- if id, err = w.styleFor(c.Format, c.Style); err != nil {153- return excelize.Cell{}, err154- }155- }156- xc := excelize.Cell{StyleID: id, Value: xlsxValue(c.Value, c.Format)}157- if !c.Formula {158- return xc, nil159- }160- if fx, ok := toExcelFormula(c.Input); ok {161- xc.Formula = fx162- return xc, nil163- }164- w.values++165- if w.example == "" {166- w.example = a.String()167- if w.multi {168- w.example = sheet.QuoteSheet(ws) + "!" + a.String()169- }144+func writePart(zw *zip.Writer, name, body string) error {145+ w, err := zw.Create(name)146+ if err != nil {147+ return err170148 }171- return xc, nil149+ _, err = io.WriteString(w, body)150+ return err172151 }173152 174-// xlsxValue is a computed value as excelize writes it.175-func xlsxValue(v sheet.Value, f sheet.Format) any {176- switch v.Kind {177- case sheet.Number:178- return toExcelSerial(v.Num, f)179- case sheet.Bool:180- return v.Num != 0181- case sheet.Text, sheet.Error:182- return v.Str153+// writeSheet writes snapshot sn as worksheet part i and returns the rows154+// written.155+func (w *xlsxWriter) writeSheet(zw *zip.Writer, i int, ws string, sn *Snapshot, active bool) (int, error) {156+ part, err := zw.Create("xl/worksheets/sheet" + strconv.Itoa(i+1) + ".xml")157+ if err != nil {158+ return 0, err183159 }184- return nil160+ bw := bufio.NewWriterSize(part, 64<<10)161+ rows := w.sheet(bw, ws, sn, active)162+ return rows, bw.Flush()185163 }186164 187165 // sheetName makes a valid Excel sheet name: at most 31 characters, none@@ -328,3 +328,66 @@ func TestXLSXWorkbookRoundTrip(t *testing.T) {
328328 t.Errorf("name Rent: %v %s", ok, n.Ref())329329 }330330 }331+332+// Text that XML can't hold, or that reads as one of SpreadsheetML's333+// escapes, goes out escaped and comes back the same, through excelize334+// and 012; text past Excel's limit is cut.335+func TestXLSXWriteText(t *testing.T) {336+ src := sheet.New()337+ long := strings.Repeat("x", excelMaxText+10)338+ want := map[string]string{339+ "A1": "bell\x07 and nul\x00", "A2": "_x0041_ stays", "A3": `<&> "quoted" 'too'`,340+ "A4": "cr\rend", "A5": "￾ noncharacter", "A6": long, "A7": "emoji \U0001F642",341+ }342+ for cell, text := range want {343+ if err := src.Load(addr(t, cell), "'"+text, sheet.Format{}, sheet.Style{}); err != nil {344+ t.Fatal(err)345+ }346+ }347+ src.RecalcAll()348+ for cell := range want {349+ want[cell] = src.Value(addr(t, cell)).Str // as 012 keeps it350+ }351+ want["A6"] = long[:excelMaxText]352+ name := filepath.Join(t.TempDir(), "text.xlsx")353+ if _, err := Export(context.Background(), name, XLSX, SnapBook(src), ExportOptions{}); err != nil {354+ t.Fatal(err)355+ }356+ x, err := excelize.OpenFile(name)357+ if err != nil {358+ t.Fatal(err)359+ }360+ defer x.Close()361+ for cell, text := range want {362+ if got, _ := x.GetCellValue("Sheet1", cell); got != text {363+ t.Errorf("excelize reads %s as %q, want %q", cell, trim(got), trim(text))364+ }365+ }366+ res, err := Import(context.Background(), name, Options{})367+ if err != nil {368+ t.Fatal(err)369+ }370+ back := res.Sheet.Book().Sheet(0)371+ for cell, text := range want {372+ if got := input(back, addr(t, cell)); got != flatten(text) {373+ t.Errorf("012 reads %s as %q, want %q", cell, trim(got), trim(text))374+ }375+ }376+}377+378+func TestUniqueSheetName(t *testing.T) {379+ used := map[string]bool{}380+ long := strings.Repeat("x", 31)381+ for _, tc := range [][2]string{{"Plan", "Plan"}, {"plan", "plan (2)"}, {"Plan", "Plan (3)"}, {long, long}, {long, long[:27] + " (2)"}} {382+ if got := uniqueSheetName(tc[0], used); got != tc[1] {383+ t.Errorf("uniqueSheetName(%q) = %q, want %q", tc[0], got, tc[1])384+ }385+ }386+}387+388+func trim(s string) string {389+ if len(s) > 40 {390+ return s[:40] + "..."391+ }392+ return s393+}@@ -0,0 +1,316 @@
1+package fileio2+3+import (4+ "bufio"5+ "fmt"6+ "math"7+ "slices"8+ "strconv"9+ "strings"10+ "unicode/utf8"11+12+ "github.com/FelineStateMachine/012/internal/sheet"13+)14+15+// excelMaxText is the most characters an Excel cell holds; longer text16+// is cut, or Excel reports the file as damaged.17+const excelMaxText = 3276718+19+// xlsxWriter writes worksheets, sharing cell formats among them.20+type xlsxWriter struct {21+ styles *xlsxStyleTable22+ values int // formulas written as values23+ example string // the first of them24+ multi bool // examples name their sheet25+ buf []byte // a row's cells26+}27+28+// sheet writes a snapshot as a worksheet and returns the rows written.29+// Errors stick in bw.30+func (w *xlsxWriter) sheet(bw *bufio.Writer, ws string, snap *Snapshot, active bool) int {31+ r := snap.Range32+ bw.WriteString(xmlHead + `<worksheet xmlns="` + sheetMain + `" xmlns:r="` + officeRel + `">`)33+ fmt.Fprintf(bw, `<dimension ref="%s%d:%s%d"/>`, excelColName(r.From.Col+1), r.From.Row+1, excelColName(r.To.Col+1), r.To.Row+1)34+ bw.WriteString(`<sheetViews><sheetView workbookViewId="0"`)35+ if active {36+ bw.WriteString(` tabSelected="1"`)37+ }38+ bw.WriteString(`/></sheetViews><sheetFormatPr defaultRowHeight="15"/>`)39+ if len(snap.Widths) > 0 {40+ bw.WriteString(`<cols>`)41+ for _, c := range slices.Sorted(mapKeys(snap.Widths)) {42+ fmt.Fprintf(bw, `<col min="%d" max="%d" width="%d" customWidth="1"/>`, c+1, c+1, max(snap.Widths[c]-excelPadding, 1))43+ }44+ bw.WriteString(`</cols>`)45+ }46+ bw.WriteString(`<sheetData>`)47+ for row := r.From.Row; row <= r.To.Row; row++ {48+ b := w.buf[:0]49+ for col := r.From.Col; col <= r.To.Col; col++ {50+ a := sheet.Addr{Col: col, Row: row}51+ if c, ok := snap.Cells[a]; ok {52+ b = w.cell(b, ws, a, c)53+ }54+ }55+ if len(b) > 0 {56+ fmt.Fprintf(bw, `<row r="%d">`, row+1)57+ bw.Write(b)58+ bw.WriteString(`</row>`)59+ }60+ w.buf = b61+ }62+ bw.WriteString(`</sheetData></worksheet>`)63+ return r.To.Row - r.From.Row + 164+}65+66+func mapKeys(m map[int]int) func(func(int) bool) {67+ return func(yield func(int) bool) {68+ for k := range m {69+ if !yield(k) {70+ return71+ }72+ }73+ }74+}75+76+// cell appends snapshot cell c at a as a <c> element. A formula's value77+// is written as its cached result; a formula with no Excel equivalent78+// is written as its value alone, and counted.79+func (w *xlsxWriter) cell(b []byte, ws string, a sheet.Addr, c SnapCell) []byte {80+ formula := ""81+ if c.Formula {82+ if fx, ok := toExcelFormula(c.Input); ok {83+ formula = fx84+ } else if w.values++; w.example == "" {85+ w.example = a.String()86+ if w.multi {87+ w.example = sheet.QuoteSheet(ws) + "!" + a.String()88+ }89+ }90+ }91+ b = append(b, `<c r="`...)92+ b = append(b, excelColName(a.Col+1)...)93+ b = strconv.AppendInt(b, int64(a.Row+1), 10)94+ b = append(b, '"')95+ // A formula's inferred format is written too, so its result shows96+ // the same in Excel.97+ if !c.Format.IsZero() || !c.Style.IsZero() {98+ b = append(b, ` s="`...)99+ b = strconv.AppendInt(b, int64(w.styles.id(c.Format, c.Style)), 10)100+ b = append(b, '"')101+ }102+ typ, v, inline := cellValue(c)103+ if typ != "" {104+ b = append(b, ` t="`...)105+ b = append(b, typ...)106+ b = append(b, '"')107+ }108+ if formula == "" && v == "" {109+ return append(b, "/>"...)110+ }111+ b = append(b, '>')112+ if formula != "" {113+ b = append(b, "<f>"...)114+ b = appendEscaped(b, formula, false)115+ b = append(b, "</f>"...)116+ }117+ switch {118+ case inline && formula == "":119+ b = append(b, `<is><t xml:space="preserve">`...)120+ b = appendEscaped(b, v, true)121+ b = append(b, "</t></is>"...)122+ case v != "":123+ b = append(b, "<v>"...)124+ b = appendEscaped(b, v, typ == "str")125+ b = append(b, "</v>"...)126+ }127+ return append(b, "</c>"...)128+}129+130+// cellValue is a cell's value as Excel stores it: the type attribute,131+// the text, and whether text goes inline (<is>) rather than in <v>,132+// which only a formula's cached text does (t="str"). Errors are written133+// as text, as Excel recalculates on opening.134+func cellValue(c SnapCell) (typ, v string, inline bool) {135+ switch c.Value.Kind {136+ case sheet.Number:137+ n := toExcelSerial(c.Value.Num, c.Format)138+ if math.IsNaN(n) || math.IsInf(n, 0) {139+ return "e", "#NUM!", false140+ }141+ return "", numInput(n), false142+ case sheet.Bool:143+ if c.Value.Num != 0 {144+ return "b", "1", false145+ }146+ return "b", "0", false147+ case sheet.Text, sheet.Error:148+ s := c.Value.Str149+ if utf8.RuneCountInString(s) > excelMaxText {150+ s = string([]rune(s)[:excelMaxText])151+ }152+ if c.Formula {153+ if _, ok := toExcelFormula(c.Input); ok {154+ return "str", s, false155+ }156+ }157+ return "inlineStr", s, true158+ }159+ return "", "", false160+}161+162+// escapeXML escapes s for XML text or, with attr, an attribute value.163+func escapeXML(s string, attr bool) string {164+ return string(appendXML(nil, s, attr))165+}166+167+func appendXML(b []byte, s string, attr bool) []byte {168+ for _, r := range s {169+ switch {170+ case r == '&':171+ b = append(b, "&"...)172+ case r == '<':173+ b = append(b, "<"...)174+ case r == '>':175+ b = append(b, ">"...)176+ case r == '"' && attr:177+ b = append(b, """...)178+ case r == '\r' || attr && (r == '\n' || r == '\t'):179+ b = append(b, "&#x"...)180+ b = strconv.AppendInt(b, int64(r), 16)181+ b = append(b, ';')182+ case !xmlChar(r):183+ b = utf8.AppendRune(b, utf8.RuneError)184+ default:185+ b = utf8.AppendRune(b, r)186+ }187+ }188+ return b189+}190+191+// appendEscaped appends s as XML text; with ooxml, as SpreadsheetML cell192+// text, where characters XML can't hold are written _xHHHH_ and a _x193+// that would read as such an escape is itself escaped (_x005F_).194+func appendEscaped(b []byte, s string, ooxml bool) []byte {195+ if !ooxml {196+ return appendXML(b, s, false)197+ }198+ for i := 0; i < len(s); {199+ r, size := utf8.DecodeRuneInString(s[i:])200+ switch {201+ case r == '_' && isOOXMLEscape(s[i:]):202+ b = append(b, "_x005F"...)203+ case !xmlChar(r) && r != utf8.RuneError && r <= 0xFFFF:204+ b = fmt.Appendf(b, "_x%04X_", r)205+ i += size206+ continue207+ }208+ b = appendXML(b, s[i:i+size], false)209+ i += size210+ }211+ return b212+}213+214+func isOOXMLEscape(s string) bool {215+ _, ok := ooxmlEscape([]byte(s[:min(len(s), 7)]))216+ return ok217+}218+219+// xmlChar reports whether XML 1.0 allows r in a document.220+func xmlChar(r rune) bool {221+ return r == '\t' || r == '\n' || r == '\r' ||222+ r >= 0x20 && r <= 0xD7FF || r >= 0xE000 && r <= 0xFFFD || r >= 0x10000 && r <= 0x10FFFF223+}224+225+// xlsxStyleTable collects the cell formats a workbook uses: each226+// distinct number format and text style is a cellXfs entry, with its227+// number format code and font.228+type xlsxStyleTable struct {229+ ids map[xlsxStyle]int230+ xfs []xlsxStyle // index 0 is the default231+ codes []string // custom number formats, from id 164232+ fonts []sheet.Style233+}234+235+func newXLSXStyleTable() *xlsxStyleTable {236+ return &xlsxStyleTable{ids: map[xlsxStyle]int{{}: 0}, xfs: []xlsxStyle{{}}, fonts: []sheet.Style{{}}}237+}238+239+// id is the index of the cell format for f and st.240+func (t *xlsxStyleTable) id(f sheet.Format, st sheet.Style) int {241+ key := xlsxStyle{f, st}242+ if id, ok := t.ids[key]; ok {243+ return id244+ }245+ id := len(t.xfs)246+ t.ids[key] = id247+ t.xfs = append(t.xfs, key)248+ return id249+}250+251+// xml is the styles part.252+func (t *xlsxStyleTable) xml() string {253+ var xfs strings.Builder254+ for _, x := range t.xfs {255+ numFmt := 0256+ if code := excelCode(x.format); code != "" {257+ i := slices.Index(t.codes, code)258+ if i < 0 {259+ i = len(t.codes)260+ t.codes = append(t.codes, code)261+ }262+ numFmt = 164 + i263+ }264+ font := fontOf(x.style)265+ i := slices.Index(t.fonts, font)266+ if i < 0 {267+ i = len(t.fonts)268+ t.fonts = append(t.fonts, font)269+ }270+ fmt.Fprintf(&xfs, `<xf numFmtId="%d" fontId="%d" fillId="0" borderId="0" xfId="0"`, numFmt, i)271+ if numFmt != 0 {272+ xfs.WriteString(` applyNumberFormat="1"`)273+ }274+ if i != 0 {275+ xfs.WriteString(` applyFont="1"`)276+ }277+ if x.style.Align == sheet.AlignAuto {278+ xfs.WriteString(`/>`)279+ continue280+ }281+ fmt.Fprintf(&xfs, ` applyAlignment="1"><alignment horizontal="%s"/></xf>`, x.style.Align)282+ }283+ var b strings.Builder284+ b.WriteString(xmlHead + `<styleSheet xmlns="` + sheetMain + `">`)285+ if len(t.codes) > 0 {286+ fmt.Fprintf(&b, `<numFmts count="%d">`, len(t.codes))287+ for i, code := range t.codes {288+ fmt.Fprintf(&b, `<numFmt numFmtId="%d" formatCode="%s"/>`, 164+i, escapeXML(code, true))289+ }290+ b.WriteString(`</numFmts>`)291+ }292+ fmt.Fprintf(&b, `<fonts count="%d">`, len(t.fonts))293+ for _, f := range t.fonts {294+ b.WriteString(`<font>`)295+ for _, on := range []struct {296+ set bool297+ tag string298+ }{{f.Bold, `<b/>`}, {f.Italic, `<i/>`}, {f.Strikethrough, `<strike/>`}, {f.Underline, `<u/>`}} {299+ if on.set {300+ b.WriteString(on.tag)301+ }302+ }303+ b.WriteString(`<sz val="11"/><name val="Calibri"/><family val="2"/></font>`)304+ }305+ b.WriteString(`</fonts><fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>` +306+ `<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>` +307+ `<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>`)308+ fmt.Fprintf(&b, `<cellXfs count="%d">%s</cellXfs>`, len(t.xfs), xfs.String())309+ b.WriteString(`<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`)310+ return b.String()311+}312+313+// fontOf is the part of a style a font holds.314+func fontOf(st sheet.Style) sheet.Style {315+ return sheet.Style{Bold: st.Bold, Italic: st.Italic, Underline: st.Underline, Strikethrough: st.Strikethrough}316+}