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 / xlsxbook.go
xlsxbook.go 7.4K
1package fileio23import (4 "bytes"5 "encoding/xml"6 "fmt"7 "io"8 "path"9 "strconv"10)1112// xlsxBook is an open workbook: its sheets, names, shared strings and13// cell styles. Worksheets are streamed one at a time by readSheet.14type xlsxBook struct {15 pkg *xlsxPackage16 sheets []xlsxSheetInfo17 active int // index of the sheet shown in Excel18 names []xlsxName19 date1904 bool20 sst sharedStrings21 styles xlsxStyles22 expanded int64 // bytes of shared formulas expanded, see formula23 // protected names the sheets read so far that Excel protects.24 protected []string25 skips ruleSkips // rules left out, see xlsxrulesimport.go26 // tablesLeft counts the tables left out, see xlsxtables.go.27 tablesLeft int28}2930// xlsxSheetInfo is a sheet as the workbook lists it.31type xlsxSheetInfo struct {32 name string33 id int // sheetId34 part string // the worksheet part, "" when missing35 kind string // the relationship type: worksheet, chartsheet...36 hidden bool // state hidden or veryHidden37}3839// xlsxName is a defined name: sheet is the index of the sheet it is40// scoped to, or -1 for the workbook.41type xlsxName struct {42 name, refersTo string43 sheet int44}4546// openXLSX opens a workbook and reads everything but its worksheets.47func openXLSX(r io.ReaderAt, size int64, lim xlsxLimits) (*xlsxBook, error) {48 p, err := openXLSXPackage(r, size, lim)49 if err != nil {50 return nil, err51 }52 bk := &xlsxBook{pkg: p}53 wb := "xl/workbook.xml"54 root, err := p.rels("")55 if err != nil {56 return nil, err57 }58 for _, rel := range root {59 if rel.typ == "officeDocument" && p.has(rel.target) {60 wb = rel.target61 }62 }63 rels, err := p.rels(wb)64 if err != nil {65 return nil, err66 }67 ids, err := bk.readWorkbook(wb)68 if err != nil {69 return nil, err70 }71 dir := path.Dir(wb) + "/"72 sst, styles := dir+"sharedStrings.xml", dir+"styles.xml"73 for _, rel := range rels {74 switch rel.typ {75 case "sharedStrings":76 sst = rel.target77 case "styles":78 styles = rel.target79 }80 }81 for i, id := range ids {82 if rel, ok := rels[id]; ok && p.has(rel.target) {83 bk.sheets[i].part, bk.sheets[i].kind = rel.target, rel.typ84 }85 }86 if err := bk.sst.read(p, sst); err != nil {87 return nil, err88 }89 if err := bk.styles.read(p, styles); err != nil {90 return nil, err91 }92 return bk, nil93}9495// readWorkbook reads the workbook part: its sheets (returning their96// relationship ids), the active tab, defined names and date system.97func (bk *xlsxBook) readWorkbook(part string) (ids []string, err error) {98 x, err := bk.pkg.open(part)99 if err != nil {100 return nil, err101 }102 if x == nil {103 return nil, fmt.Errorf("not an Excel workbook: no %s", part)104 }105 defer x.close()106 activeTab, views := 0, 0107 for {108 t, err := x.next()109 if err == io.EOF {110 break111 }112 if err != nil {113 return nil, err114 }115 se, ok := t.(xml.StartElement)116 if !ok {117 continue118 }119 switch se.Name.Local {120 case "sheet":121 if len(bk.sheets) >= bk.pkg.lim.sheets {122 return nil, fmt.Errorf("the workbook has more than %d sheets: %w", bk.pkg.lim.sheets, errXLSXLimit)123 }124 name, _ := attr(se, "name")125 id, _ := attr(se, "sheetId")126 state, _ := attr(se, "state")127 n, _ := strconv.Atoi(id)128 bk.sheets = append(bk.sheets, xlsxSheetInfo{name: name, id: n, hidden: state == "hidden" || state == "veryHidden"})129 ids = append(ids, relAttr(se))130 case "workbookView":131 if views++; views == 1 {132 v, _ := attr(se, "activeTab")133 activeTab, _ = strconv.Atoi(v)134 }135 case "workbookPr":136 v, _ := attr(se, "date1904")137 bk.date1904 = v == "1" || v == "true"138 case "definedName":139 if err := bk.readName(x, se); err != nil {140 return nil, err141 }142 }143 }144 bk.active = bk.activeIndex(activeTab)145 return ids, nil146}147148// activeIndex is the index of the sheet Excel showed, as excelize finds149// it: the active tab's sheet by its sheetId, else the first.150func (bk *xlsxBook) activeIndex(tab int) int {151 if len(bk.sheets) == 0 {152 return 0153 }154 id := bk.sheets[0].id155 if tab >= 0 && tab < len(bk.sheets) && bk.sheets[tab].id != 0 {156 id = bk.sheets[tab].id157 }158 for i, s := range bk.sheets {159 if s.id == id {160 return i161 }162 }163 return 0164}165166func (bk *xlsxBook) readName(x *xmlStream, se xml.StartElement) error {167 if len(bk.names) >= bk.pkg.lim.styles {168 return fmt.Errorf("the workbook has more than %d names: %w", bk.pkg.lim.styles, errXLSXLimit)169 }170 n := xlsxName{sheet: -1}171 n.name, _ = attr(se, "name")172 if v, ok := attr(se, "localSheetId"); ok {173 if id, err := strconv.Atoi(v); err == nil && id >= 0 {174 n.sheet = id175 }176 }177 text, err := x.text(nil)178 n.refersTo = string(text)179 bk.names = append(bk.names, n)180 return err181}182183// sharedStrings holds the workbook's shared strings end to end in one184// buffer, so a table of many short strings costs its text and four185// bytes a string rather than a string header and allocation each.186type sharedStrings struct {187 buf []byte188 ends []uint32189}190191func (s *sharedStrings) get(i int) (string, bool) {192 if i < 0 || i >= len(s.ends) {193 return "", false194 }195 from := uint32(0)196 if i > 0 {197 from = s.ends[i-1]198 }199 return string(s.buf[from:s.ends[i]]), true200}201202// read streams the shared strings part: each <si> is its <t> text, or203// the text of its rich-text runs (<r><t>), leaving out phonetic hints.204func (s *sharedStrings) read(p *xlsxPackage, part string) error {205 x, err := p.open(part)206 if x == nil || err != nil {207 return err208 }209 defer x.close()210 var item []byte211 for {212 t, err := x.next()213 if err == io.EOF {214 return nil215 }216 if err != nil {217 return err218 }219 se, ok := t.(xml.StartElement)220 if !ok || se.Name.Local != "si" {221 continue222 }223 if item, err = inlineText(x, item[:0]); err != nil {224 return err225 }226 if len(s.ends) >= p.lim.strings || len(s.buf)+len(item) > 1<<32-1 {227 return fmt.Errorf("the workbook has more than %d shared strings: %w", p.lim.strings, errXLSXLimit)228 }229 s.buf = append(s.buf, unescapeOOXML(item)...)230 s.ends = append(s.ends, uint32(len(s.buf)))231 }232}233234// inlineText reads the rest of an <si> or <is> element: the text of its235// <t> children and of the <t> in its <r> runs, not in <rPh> hints.236func inlineText(x *xmlStream, buf []byte) ([]byte, error) {237 depth := x.depth238 inT, skipDepth := false, 0239 for {240 t, err := x.next()241 if err != nil {242 return buf, eofAsUnexpected(err)243 }244 switch t := t.(type) {245 case xml.StartElement:246 switch {247 case skipDepth > 0:248 case t.Name.Local == "rPh" || t.Name.Local == "phoneticPr":249 skipDepth = x.depth250 case t.Name.Local == "t":251 inT = true252 }253 case xml.EndElement:254 if x.depth < depth {255 return buf, nil256 }257 if skipDepth > 0 && x.depth < skipDepth {258 skipDepth = 0259 }260 inT = false261 case xml.CharData:262 if inT && skipDepth == 0 {263 buf = append(buf, t...)264 }265 }266 }267}268269// unescapeOOXML decodes _xHHHH_, how OOXML writes characters XML can't270// hold (_x000D_ for a carriage return), and _x005F_, an escaped _.271func unescapeOOXML(b []byte) []byte {272 if !bytes.Contains(b, []byte("_x")) {273 return b274 }275 out := make([]byte, 0, len(b))276 for i := 0; i < len(b); {277 if r, ok := ooxmlEscape(b[i:]); ok {278 if r == '_' {279 out = append(out, '_')280 } else {281 out = append(out, string(r)...)282 }283 i += 7284 continue285 }286 out = append(out, b[i])287 i++288 }289 return out290}291292// ooxmlEscape decodes the _xHHHH_ at the start of b.293func ooxmlEscape(b []byte) (rune, bool) {294 if len(b) < 7 || b[0] != '_' || b[1] != 'x' || b[6] != '_' {295 return 0, false296 }297 v, err := strconv.ParseUint(string(b[2:6]), 16, 16)298 if err != nil {299 return 0, false300 }301 return rune(v), true302}