cat / 012
clone

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

main
012 / internal / fileio / xlsxtables.go
xlsxtables.go 6.7K
1package fileio23import (4 "archive/zip"5 "bufio"6 "encoding/xml"7 "fmt"8 "io"9 "strconv"10 "strings"1112 "src.rest/cat/012/internal/sheet"13)1415// Tables travel as Excel's tables: a table part per table, related to16// its worksheet, which lists them in <tableParts>. A filter on a table's17// range goes in the table part, as Excel keeps a table's filter, rather18// than on the sheet. A banded table has row stripes, and a styled header19// Excel's default table style; a table with neither has no style.20// Excel needs a table's header cells to hold text, so a number there is21// written as the text it shows.2223const (24 tableType = mlType + "table+xml"25 tableRelID = "rIdTable"26 tableStyle = "TableStyleMedium2"27 maxTableCol = 16384 // tableColumns Excel reads, a sheet's width28)2930// tableFilter is the index of the table whose range the sheet's filter31// has, or -1.32func tableFilter(snap *Snapshot) int {33 if snap.Filter == nil {34 return -135 }36 for i, t := range snap.Tables {37 if t.Range == snap.Filter.Range {38 return i39 }40 }41 return -142}4344// headerCells are the cells of the header rows of the snapshot's45// tables.46func headerCells(snap *Snapshot) map[sheet.Addr]bool {47 if len(snap.Tables) == 0 {48 return nil49 }50 out := map[sheet.Addr]bool{}51 for _, t := range snap.Tables {52 for c := t.Range.From.Col; c <= t.Range.To.Col; c++ {53 out[sheet.Addr{Col: c, Row: t.Range.From.Row}] = true54 }55 }56 return out57}5859// writeTableParts lists the snapshot's tables in its worksheet, by60// their relationships.61func writeTableParts(bw *bufio.Writer, snap *Snapshot) {62 if len(snap.Tables) == 0 {63 return64 }65 fmt.Fprintf(bw, `<tableParts count="%d">`, len(snap.Tables))66 for i := range snap.Tables {67 fmt.Fprintf(bw, `<tablePart r:id="%s%d"/>`, tableRelID, i+1)68 }69 bw.WriteString(`</tableParts>`)70}7172// writeTables writes the table parts of a worksheet's tables, numbered73// on from the tables written before, and returns their relationships74// from the worksheet.75func (w *xlsxWriter) writeTables(zw *zip.Writer, snap *Snapshot) ([]string, error) {76 var rels []string77 onFilter := tableFilter(snap)78 for i, t := range snap.Tables {79 w.tableParts++80 n := strconv.Itoa(w.tableParts)81 var body strings.Builder82 bw := bufio.NewWriter(&body)83 fmt.Fprintf(bw, xmlHead+`<table xmlns="%s" id="%s" name="%s" displayName="%s" ref="%s" totalsRowShown="0">`,84 sheetMain, n, escapeXML(t.Name, true), escapeXML(t.Name, true), excelRect(t.Range))85 if i == onFilter {86 writeAutoFilter(bw, snap)87 }88 fmt.Fprintf(bw, `<tableColumns count="%d">`, len(t.Cols))89 for j, col := range t.Cols {90 fmt.Fprintf(bw, `<tableColumn id="%d" name="%s"/>`, j+1, escapeXML(col, true))91 }92 bw.WriteString(`</tableColumns><tableStyleInfo`)93 if t.Header {94 bw.WriteString(` name="` + tableStyle + `"`)95 }96 fmt.Fprintf(bw, ` showFirstColumn="0" showLastColumn="0" showRowStripes="%d" showColumnStripes="0"/></table>`, b2i(t.Banded))97 if err := bw.Flush(); err != nil {98 return nil, err99 }100 if err := writePart(zw, "xl/tables/table"+n+".xml", body.String()); err != nil {101 return nil, err102 }103 w.tableTypes = append(w.tableTypes, `<Override PartName="/xl/tables/table`+n+`.xml" ContentType="`+tableType+`"/>`)104 rels = append(rels, fmt.Sprintf(`<Relationship Id="%s%d" Type="%s/table" Target="../tables/table%s.xml"/>`, tableRelID, i+1, officeRel, n))105 }106 return rels, nil107}108109func b2i(b bool) int {110 if b {111 return 1112 }113 return 0114}115116// tableKeys are the keys of the tables the snapshots hold, which117// formulas may read in the file.118func tableKeys(sheets []*Snapshot) map[string]bool {119 out := map[string]bool{}120 for _, sn := range sheets {121 for _, t := range sn.Tables {122 out[strings.ToUpper(t.Name)] = true123 }124 }125 return out126}127128// unknownTable reports whether c's formula reads a table the file129// doesn't hold, such as a notebook output's, which Excel would refuse.130func (w *xlsxWriter) unknownTable(c SnapCell) bool {131 for _, name := range c.Tables {132 if !w.tables[strings.ToUpper(name)] {133 return true134 }135 }136 return false137}138139// tablesNote says how many tables an import left out, or nothing.140func (bk *xlsxBook) tablesNote() []string {141 if bk.tablesLeft == 0 {142 return nil143 }144 return []string{count(bk.tablesLeft, "table", "tables") + " left out: without a header row, or named as a range or another table is"}145}146147// readTables loads the tables of worksheet part, from the table parts148// its relationships name, into s, whose cells are read. A table's149// filter goes in af when the sheet has none of its own. Tables 012150// can't hold (no header row, a name taken) are left out, and counted.151func (bk *xlsxBook) readTables(s *sheet.Sheet, part string, af **xlsxAutoFilter) error {152 rels, err := bk.pkg.rels(part)153 if err != nil {154 return err155 }156 for _, rel := range rels {157 if rel.typ != "table" || !bk.pkg.has(rel.target) {158 continue159 }160 t, tf, err := bk.readTable(rel.target)161 if err != nil {162 return fmt.Errorf("table: %w", err)163 }164 if t == nil || s.LoadTable(*t) != nil {165 bk.tablesLeft++166 continue167 }168 if *af == nil && tf != nil {169 *af = tf170 }171 }172 return nil173}174175// readTable reads a table part: its name, range, columns and style,176// and its filter; nil for a table without a header row. A totals row177// is left as cells below the table.178func (bk *xlsxBook) readTable(part string) (*sheet.Table, *xlsxAutoFilter, error) {179 x, err := bk.pkg.open(part)180 if x == nil || err != nil {181 return nil, nil, err182 }183 defer x.close()184 var t sheet.Table185 var af *xlsxAutoFilter186 for {187 tok, err := x.next()188 if err == io.EOF {189 break190 }191 if err != nil {192 return nil, nil, err193 }194 se, ok := tok.(xml.StartElement)195 if !ok {196 continue197 }198 switch se.Name.Local {199 case "table":200 if !tableHead(&t, se) {201 return nil, nil, x.skip()202 }203 case "autoFilter":204 if af, err = readAutoFilter(x, se); err != nil {205 return nil, nil, err206 }207 case "tableColumn":208 if len(t.Cols) < maxTableCol {209 t.Cols = append(t.Cols, attrOr(se, "name", ""))210 }211 case "tableStyleInfo":212 t.Header = attrOr(se, "name", "") != ""213 t.Banded = boolAttr(se, "showRowStripes", false)214 }215 }216 if t.Range.To.Row == t.Range.From.Row {217 return nil, nil, nil218 }219 return &t, af, nil220}221222// tableHead reads a <table>'s name and range into t, the range less a223// totals row, and reports false for a table 012 can't hold: no header224// row, or no range. LoadTable checks the name.225func tableHead(t *sheet.Table, se xml.StartElement) bool {226 t.Name = attrOr(se, "displayName", attrOr(se, "name", ""))227 r, ok := sheet.ParseRange(strings.ReplaceAll(attrOr(se, "ref", ""), "$", ""))228 if !ok || attrOr(se, "headerRowCount", "1") == "0" {229 return false230 }231 if n, err := strconv.Atoi(attrOr(se, "totalsRowCount", "0")); err == nil && n > 0 {232 r.To.Row = max(r.To.Row-n, r.From.Row)233 }234 t.Range = r235 return true236}