1package fileio23import (4 "bufio"5 "encoding/xml"6 "fmt"7 "math"8 "slices"9 "strings"1011 "src.rest/cat/012/internal/sheet"12)1314// Wrapping, borders, row heights and merged cells in XLSX, both ways.15// Wrapped text is a cell format's alignment wrapText; clipping has no16// XLSX form, so clipped text overflows in Excel. Borders are the styles17// part's <borders>, a cell format pointing at one: Excel's hair, thin,18// dotted and dashed lines read as thin, medium and thick ones as thick,19// double as double, and 012's thick is written as medium, the weight20// Sheets' middle line has. A row's height is in points, 15 to a line,21// written with customHeight as a height set by hand; only those are22// read, as Excel sizes the others to fit. Merged cells are <mergeCells>23// after the cells.2425// pointsPerLine is the height of a line of text in points: Excel's26// default row height.27const pointsPerLine = 152829// borderLine maps an Excel border style to a line.30func borderLine(style string) sheet.Line {31 switch style {32 case "", "none":33 return sheet.LineNone34 case "double":35 return sheet.LineDouble36 case "medium", "thick", "mediumDashed", "mediumDashDot", "mediumDashDotDot", "slantDashDot":37 return sheet.LineThick38 }39 return sheet.LineThin40}4142// excelBorderStyle is the Excel border style that draws l.43func excelBorderStyle(l sheet.Line) string {44 switch l {45 case sheet.LineThick:46 return "medium"47 case sheet.LineDouble:48 return "double"49 }50 return "thin"51}5253// xlsxEdges are a <border>'s elements for each edge; start and end are54// left and right in left-to-right sheets.55var xlsxEdges = map[string]sheet.Edge{56 "top": sheet.EdgeTop, "bottom": sheet.EdgeBottom, "left": sheet.EdgeLeft, "start": sheet.EdgeLeft, "right": sheet.EdgeRight, "end": sheet.EdgeRight,57}5859// excelVAligns are Excel's vertical alignments; justify and distributed60// have no match and sit where 012's text sits by default.61var excelVAligns = map[string]sheet.VAlign{"top": sheet.VAlignTop, "center": sheet.VAlignMiddle, "bottom": sheet.VAlignBottom}6263// excelVAlign is the vertical alignment Excel writes for v.64func excelVAlign(v sheet.VAlign) string {65 for name, va := range excelVAligns {66 if va == v {67 return name68 }69 }70 return ""71}7273// readBorder reads the rest of a <border>: its four edges, each with74// its line's color as the nearest named color (see xlsxColor.named).75func readBorder(x *xmlStream) (sheet.Borders, error) {76 var b sheet.Borders77 depth := x.depth78 edge, inEdge := sheet.EdgeTop, false79 for {80 t, err := x.next()81 if err != nil {82 return b, eofAsUnexpected(err)83 }84 switch t := t.(type) {85 case xml.StartElement:86 switch {87 case x.depth == depth+1:88 edge, inEdge = xlsxEdges[t.Name.Local]89 if inEdge {90 b = b.With(edge, borderLine(attrOr(t, "style", "")))91 }92 case x.depth == depth+2 && inEdge && t.Name.Local == "color" && b.Line(edge) != sheet.LineNone:93 b = b.WithColor(edge, readColor(t).named())94 }95 case xml.EndElement:96 if x.depth < depth {97 return b, nil98 }99 }100 }101}102103// borderID is the index of the <border> drawing b, 0 for none.104func (t *xlsxStyleTable) borderID(b sheet.Borders) int {105 if i := slices.Index(t.borders, b); i >= 0 {106 return i107 }108 t.borders = append(t.borders, b)109 return len(t.borders) - 1110}111112// bordersXML writes the <borders>, the first drawing none.113func (t *xlsxStyleTable) bordersXML() string {114 var b strings.Builder115 fmt.Fprintf(&b, `<borders count="%d">`, len(t.borders))116 for _, br := range t.borders {117 b.WriteString(`<border>`)118 for _, e := range [...]struct {119 tag string120 l sheet.Line121 edge sheet.Edge122 }{{"left", br.Left(), sheet.EdgeLeft}, {"right", br.Right(), sheet.EdgeRight}, {"top", br.Top(), sheet.EdgeTop}, {"bottom", br.Bottom(), sheet.EdgeBottom}} {123 if e.l == sheet.LineNone {124 fmt.Fprintf(&b, `<%s/>`, e.tag)125 continue126 }127 fmt.Fprintf(&b, `<%s style="%s">%s</%s>`, e.tag, excelBorderStyle(e.l), borderColorXML(br.Color(e.edge)), e.tag)128 }129 b.WriteString(`<diagonal/></border>`)130 }131 return b.String() + `</borders>`132}133134// borderColorXML is the <color> of a border line: a named color's, as135// rules write it, or the automatic one.136func borderColorXML(c sheet.Color) string {137 if c == sheet.ColorNone {138 return `<color auto="1"/>`139 }140 return fmt.Sprintf(`<color rgb="%s"/>`, ruleRGB[c][0])141}142143// alignmentXML is a cell format's <alignment>, or "" when it has none.144func alignmentXML(st sheet.Style) string {145 if st.Align == sheet.AlignAuto && st.Wrap != sheet.WrapOn && st.VAlign == sheet.VAlignAuto {146 return ""147 }148 s := `<alignment`149 if st.Align != sheet.AlignAuto {150 s += fmt.Sprintf(` horizontal="%s"`, st.Align)151 }152 if v := excelVAlign(st.VAlign); v != "" {153 s += fmt.Sprintf(` vertical="%s"`, v)154 }155 if st.Wrap == sheet.WrapOn {156 s += ` wrapText="1"`157 }158 return s + `/>`159}160161// rowHeight is the height in lines of a <row> set by hand to more than162// a line, and 0 for one that fits its contents.163func rowHeight(se xml.StartElement) int {164 if !boolAttr(se, "customHeight", false) {165 return 0166 }167 var ht float64168 if _, err := fmt.Sscan(attrOr(se, "ht", ""), &ht); err != nil || !(ht > 0) {169 return 0170 }171 if lines := clamp(int(math.Round(ht/pointsPerLine)), 1, sheet.MaxRowHeight); lines > 1 {172 return lines173 }174 return 0175}176177// maxMergesRead caps the merged ranges read from a worksheet.178const maxMergesRead = 1 << 16179180// writeMerges writes the sheet's <mergeCells>.181func writeMerges(bw *bufio.Writer, snap *Snapshot) {182 if len(snap.Merges) == 0 {183 return184 }185 fmt.Fprintf(bw, `<mergeCells count="%d">`, len(snap.Merges))186 for _, m := range snap.Merges {187 fmt.Fprintf(bw, `<mergeCell ref="%s"/>`, excelRect(m))188 }189 bw.WriteString(`</mergeCells>`)190}191192// readMerge reads a <mergeCell>'s range, ignoring one that isn't a193// range of the sheet.194func (r *xlsxSheetReader) readMerge(se xml.StartElement) {195 ref, _ := attr(se, "ref")196 if m, ok := sheet.ParseRange(strings.ReplaceAll(ref, "$", "")); ok && len(r.merges) < maxMergesRead {197 r.merges = append(r.merges, m)198 }199}