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 / xlsxnotes.go
xlsxnotes.go 4.7K
1package fileio23import (4 "archive/zip"5 "encoding/xml"6 "fmt"7 "io"8 "maps"9 "slices"10 "strconv"11 "strings"1213 "src.rest/cat/012/internal/sheet"14)1516// Cell notes travel as Excel's notes (legacy comments, which Sheets17// imports as notes and writes its notes as): a comments part per sheet18// with the text, and the VML drawing Excel needs to show each note's19// box, both related to the worksheet, which names the drawing in20// <legacyDrawing>.2122const (23 commentsType = mlType + "comments+xml"24 vmlType = "application/vnd.openxmlformats-officedocument.vmlDrawing"25 notesRelID = "rIdNotes"26 vmlRelID = "rIdVML"27)2829// writeNotes writes worksheet part n's notes: the comments part and the30// VML drawing, which notesRels relate to it.31func writeNotes(zw *zip.Writer, n int, notes map[sheet.Addr]string) error {32 num := strconv.Itoa(n)33 addrs := slices.SortedFunc(maps.Keys(notes), func(a, b sheet.Addr) int {34 if a.Row != b.Row {35 return a.Row - b.Row36 }37 return a.Col - b.Col38 })39 var comments, vml strings.Builder40 comments.WriteString(xmlHead + `<comments xmlns="` + sheetMain + `"><authors><author></author></authors><commentList>`)41 fmt.Fprintf(&vml, `<xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">`+42 `<o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="%d"/></o:shapelayout>`+43 `<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe">`+44 `<v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>`, n)45 for k, a := range addrs {46 comments.WriteString(`<comment ref="` + a.String() + `" authorId="0"><text><t xml:space="preserve">`)47 comments.Write(appendEscaped(nil, notes[a], true))48 comments.WriteString(`</t></text></comment>`)49 fmt.Fprintf(&vml, `<v:shape id="_x0000_s%d" type="#_x0000_t202" style="position:absolute;margin-left:60pt;margin-top:2pt;width:108pt;height:60pt;z-index:%d;visibility:hidden" fillcolor="#ffffe1" o:insetmode="auto">`+50 `<v:fill color2="#ffffe1"/><v:shadow on="t" color="black" obscured="t"/><v:path o:connecttype="none"/>`+51 `<v:textbox style="mso-direction-alt:auto"><div style="text-align:left"></div></v:textbox>`+52 `<x:ClientData ObjectType="Note"><x:MoveWithCells/><x:SizeWithCells/><x:Anchor>%d, 15, %d, 2, %d, 15, %d, 4</x:Anchor>`+53 `<x:AutoFill>False</x:AutoFill><x:Row>%d</x:Row><x:Column>%d</x:Column></x:ClientData></v:shape>`,54 n*1024+k+1, k+1, a.Col+1, a.Row, a.Col+3, a.Row+4, a.Row, a.Col)55 }56 comments.WriteString(`</commentList></comments>`)57 vml.WriteString(`</xml>`)58 for _, p := range []struct{ name, body string }{59 {"xl/comments" + num + ".xml", comments.String()},60 {"xl/drawings/vmlDrawing" + num + ".vml", vml.String()},61 } {62 if err := writePart(zw, p.name, p.body); err != nil {63 return err64 }65 }66 return nil67}6869// notesRels are the relationships from worksheet part n to its notes'70// parts.71func notesRels(n int) []string {72 num := strconv.Itoa(n)73 return []string{74 `<Relationship Id="` + notesRelID + `" Type="` + officeRel + `/comments" Target="../comments` + num + `.xml"/>`,75 `<Relationship Id="` + vmlRelID + `" Type="` + officeRel + `/vmlDrawing" Target="../drawings/vmlDrawing` + num + `.vml"/>`,76 }77}7879// notesTypes are the content types of the notes of the worksheet parts80// numbered in withNotes.81func notesTypes(withNotes []int) string {82 if len(withNotes) == 0 {83 return ""84 }85 var b strings.Builder86 b.WriteString(`<Default Extension="vml" ContentType="` + vmlType + `"/>`)87 for _, n := range withNotes {88 fmt.Fprintf(&b, `<Override PartName="/xl/comments%d.xml" ContentType="%s"/>`, n, commentsType)89 }90 return b.String()91}9293// readNotes reads the notes of worksheet part, from the comments part94// its relationships name, into s.95func (bk *xlsxBook) readNotes(s *sheet.Sheet, part string) error {96 rels, err := bk.pkg.rels(part)97 if err != nil {98 return err99 }100 for _, rel := range rels {101 if rel.typ == "comments" && bk.pkg.has(rel.target) {102 if err := bk.readComments(s, rel.target); err != nil {103 return fmt.Errorf("notes: %w", err)104 }105 }106 }107 return nil108}109110// readComments reads a comments part: each <comment ref="B2"> with the111// text of its runs.112func (bk *xlsxBook) readComments(s *sheet.Sheet, part string) error {113 x, err := bk.pkg.open(part)114 if x == nil || err != nil {115 return err116 }117 defer x.close()118 var buf []byte119 for {120 t, err := x.next()121 if err == io.EOF {122 return nil123 }124 if err != nil {125 return err126 }127 se, ok := t.(xml.StartElement)128 if !ok || se.Name.Local != "comment" {129 continue130 }131 ref, _ := attr(se, "ref")132 if buf, err = inlineText(x, buf[:0]); err != nil {133 return err134 }135 if a, ok := sheet.ParseAddr(ref); ok {136 s.LoadNote(a, string(unescapeOOXML(buf)))137 }138 }139}