cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / fileio / xlsxstyles.go
xlsxstyles.go 7.1K
1package fileio23import (4 "encoding/xml"5 "fmt"6 "io"7 "strconv"8 "strings"910 "src.rest/cat/012/internal/locale"11 "src.rest/cat/012/internal/sheet"12)1314// xlsxStyles is what 012 reads of the styles part: custom number15// formats, fonts, borders, and the cell formats (cellXfs) that cells16// refer to by index, resolved to 012 formats and styles on first use.17type xlsxStyles struct {18 numFmts map[int]string19 fonts []xlsxFont20 borders []sheet.Borders21 xfs []xlsxXf22 cache map[int]xlsxStyle23 dxfs []sheet.RuleStyle // conditional formats' styles, see xlsxrulesread.go24 // loc is the importing workbook's locale, whose Currency and Date25 // codes read as those formats (see formatIn).26 loc *locale.Locale27}2829type xlsxFont struct{ bold, italic, strike, underline bool }3031// xlsxXf is a cell format; numFmt, font and border are -1 when not32// given.33type xlsxXf struct {34 numFmt, font, border int35 applyFont, applyAlign, applyBorder bool36 align sheet.Align37 valign sheet.VAlign38 wrap bool39}4041// style is the cell format with index id, as 012 keeps it; an unknown42// id is the default.43func (s *xlsxStyles) style(id int) xlsxStyle {44 if id <= 0 || id >= len(s.xfs) {45 return xlsxStyle{}46 }47 if st, ok := s.cache[id]; ok {48 return st49 }50 xf := s.xfs[id]51 var out xlsxStyle52 out.format = s.format(xf.numFmt)53 if xf.applyFont && xf.font >= 0 && xf.font < len(s.fonts) {54 f := s.fonts[xf.font]55 out.style.Bold, out.style.Italic, out.style.Strikethrough, out.style.Underline = f.bold, f.italic, f.strike, f.underline56 }57 if xf.applyAlign {58 out.style.Align, out.style.VAlign = xf.align, xf.valign59 if xf.wrap {60 out.style.Wrap = sheet.WrapOn61 }62 }63 if xf.applyBorder && xf.border >= 0 && xf.border < len(s.borders) {64 out.style.Borders = s.borders[xf.border]65 }66 if s.cache == nil {67 s.cache = map[int]xlsxStyle{}68 }69 s.cache[id] = out70 return out71}7273// format maps a number format id to a 012 format. Built-in ids (and the74// locale-dependent ones) mean their built-in code even when the file75// defines them again; others are looked up among the custom codes.76func (s *xlsxStyles) format(id int) sheet.Format {77 if id < 0 {78 return sheet.Format{}79 }80 if builtinNumFmt(id) {81 return formatOf(id, "")82 }83 return formatIn(0, s.numFmts[id], s.loc)84}8586// builtinNumFmt reports whether id is one of Excel's built-in number87// formats: the ones every locale shares, and the ranges each locale88// defines for itself.89func builtinNumFmt(id int) bool {90 switch {91 case id >= 0 && id <= 4, id >= 9 && id <= 22, id >= 37 && id <= 49:92 return true93 case id >= 27 && id <= 36, id >= 50 && id <= 62, id >= 67 && id <= 81:94 return true95 }96 return false97}9899// read streams the styles part.100func (s *xlsxStyles) read(p *xlsxPackage, part string) error {101 x, err := p.open(part)102 if x == nil || err != nil {103 return err104 }105 defer x.close()106 s.numFmts = map[int]string{}107 section := "" // numFmts, fonts or cellXfs, at depth 2108 var cur *xlsxXf // the <xf> being read109 for {110 t, err := x.next()111 if err == io.EOF {112 return nil113 }114 if err != nil {115 return err116 }117 switch t := t.(type) {118 case xml.StartElement:119 if x.depth == 2 {120 section = t.Name.Local121 continue122 }123 if cur, err = s.element(x, t, section, cur); err != nil {124 return err125 }126 case xml.EndElement:127 if x.depth < 2 {128 section = ""129 }130 }131 }132}133134// styleLists name what each list of the styles part holds, by section135// and element, for the error when one is too long.136var styleLists = map[[2]string]string{137 {"dxfs", "dxf"}: "styles", {"numFmts", "numFmt"}: "styles", {"fonts", "font"}: "fonts",138 {"borders", "border"}: "borders", {"cellXfs", "xf"}: "cell formats",139}140141// element reads one element inside a section of the styles part.142func (s *xlsxStyles) element(x *xmlStream, se xml.StartElement, section string, cur *xlsxXf) (*xlsxXf, error) {143 full := len(s.numFmts) >= x.g.p.lim.styles || len(s.fonts) >= x.g.p.lim.styles || len(s.xfs) >= x.g.p.lim.styles ||144 len(s.dxfs) >= x.g.p.lim.styles || len(s.borders) >= x.g.p.lim.styles145 if what, listed := styleLists[[2]string{section, se.Name.Local}]; listed && full && x.depth == 3 {146 return nil, fmt.Errorf("the workbook has more than %d %s: %w", x.g.p.lim.styles, what, errXLSXLimit)147 }148 switch {149 case section == "dxfs" && se.Name.Local == "dxf" && x.depth == 3:150 st, err := readDxf(x)151 if err != nil {152 return nil, err153 }154 s.dxfs = append(s.dxfs, st)155 case section == "numFmts" && se.Name.Local == "numFmt" && x.depth == 3:156 id, err := strconv.Atoi(attrOr(se, "numFmtId", ""))157 if err == nil {158 s.numFmts[id], _ = attr(se, "formatCode")159 }160 case section == "fonts" && se.Name.Local == "font" && x.depth == 3:161 f, err := readFont(x)162 if err != nil {163 return nil, err164 }165 s.fonts = append(s.fonts, f)166 case section == "borders" && se.Name.Local == "border" && x.depth == 3:167 b, err := readBorder(x)168 if err != nil {169 return nil, err170 }171 s.borders = append(s.borders, b)172 case section == "cellXfs" && se.Name.Local == "xf" && x.depth == 3:173 s.xfs = append(s.xfs, xlsxXf{174 numFmt: intAttr(se, "numFmtId", -1), font: intAttr(se, "fontId", -1), border: intAttr(se, "borderId", -1),175 applyFont: boolAttr(se, "applyFont", true), applyAlign: boolAttr(se, "applyAlignment", true),176 applyBorder: boolAttr(se, "applyBorder", true),177 })178 return &s.xfs[len(s.xfs)-1], nil179 case section == "cellXfs" && se.Name.Local == "alignment" && x.depth == 4 && cur != nil:180 cur.wrap = boolAttr(se, "wrapText", false)181 switch h, _ := attr(se, "horizontal"); h {182 case "left":183 cur.align = sheet.AlignLeft184 case "center", "centerContinuous":185 cur.align = sheet.AlignCenter186 case "right":187 cur.align = sheet.AlignRight188 }189 cur.valign = excelVAligns[attrOr(se, "vertical", "")]190 }191 return cur, nil192}193194// readFont reads the rest of a <font>: bold, italic, strikethrough and195// underline (any but u val="none").196func readFont(x *xmlStream) (xlsxFont, error) {197 var f xlsxFont198 depth := x.depth199 for {200 t, err := x.next()201 if err != nil {202 return f, eofAsUnexpected(err)203 }204 switch t := t.(type) {205 case xml.StartElement:206 if x.depth != depth+1 {207 continue208 }209 switch t.Name.Local {210 case "b":211 f.bold = boolAttr(t, "val", true)212 case "i":213 f.italic = boolAttr(t, "val", true)214 case "strike":215 f.strike = boolAttr(t, "val", true)216 case "u":217 f.underline = attrOr(t, "val", "single") != "none"218 }219 case xml.EndElement:220 if x.depth < depth {221 return f, nil222 }223 }224 }225}226227func attrOr(se xml.StartElement, name, def string) string {228 if v, ok := attr(se, name); ok {229 return v230 }231 return def232}233234func intAttr(se xml.StartElement, name string, def int) int {235 if v, ok := attr(se, name); ok {236 if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {237 return n238 }239 }240 return def241}242243// boolAttr reads an xsd:boolean attribute; an empty value is true, and244// one that isn't a boolean is false.245func boolAttr(se xml.StartElement, name string, def bool) bool {246 v, ok := attr(se, name)247 if !ok {248 return def249 }250 if v = strings.TrimSpace(v); v == "" {251 return true252 }253 b, _ := strconv.ParseBool(v)254 return b255}