From b3509623af22a82e30612f64ec2df54026433471 Mon Sep 17 00:00:00 2001 From: Iwasa Kazmi Date: Sun, 23 Feb 2020 20:09:35 +0900 Subject: [PATCH] add support of rich text * Change xlsxSI so that it can contain "r" element. * Add xlsxT for representing "t" element. xlsxT also supports xml:space="preseve" in the serialization. * Change Cell so that it can have the rich text. It is possible to be a breaking change. * RefTable can handle rich text --- cell.go | 10 + format_code.go | 12 +- lib.go | 11 +- reftable.go | 88 +++++++-- reftable_test.go | 187 +++++++++++++++++-- richtext.go | 207 +++++++++++++++++++++ richtext_test.go | 388 +++++++++++++++++++++++++++++++++++++++ sheet.go | 2 + stream_file.go | 4 +- xmlSharedStrings.go | 136 +++++++++++++- xmlSharedStrings_test.go | 174 +++++++++++++++++- 11 files changed, 1165 insertions(+), 54 deletions(-) create mode 100644 richtext.go create mode 100644 richtext_test.go diff --git a/cell.go b/cell.go index 4635ce18..0151865c 100644 --- a/cell.go +++ b/cell.go @@ -60,6 +60,7 @@ func (ct *CellType) fallbackTo(cellData string, fallback CellType) CellType { type Cell struct { Row *Row Value string + RichText []RichTextRun formula string style *Style NumFmt string @@ -155,6 +156,15 @@ func (c *Cell) Type() CellType { // SetString sets the value of a cell to a string. func (c *Cell) SetString(s string) { c.Value = s + c.RichText = nil + c.formula = "" + c.cellType = CellTypeString +} + +// SetRichText sets the value of a cell to a set of the rich text. +func (c *Cell) SetRichText(r []RichTextRun) { + c.Value = "" + c.RichText = append([]RichTextRun(nil), r...) c.formula = "" c.cellType = CellTypeString } diff --git a/format_code.go b/format_code.go index 2c7206b6..3952b397 100644 --- a/format_code.go +++ b/format_code.go @@ -85,13 +85,19 @@ func (fullFormat *parsedNumberFormat) FormatValue(cell *Cell) (string, error) { case CellTypeInline: fallthrough case CellTypeStringFormula: + var cellValue string + if len(cell.RichText) > 0 { + cellValue = richTextToPlainText(cell.RichText) + } else { + cellValue = cell.Value + } textFormat := cell.parsedNumFmt.textFormat // This switch statement is only for String formats switch textFormat.reducedFormatString { case builtInNumFmt[builtInNumFmtIndex_GENERAL]: // General is literally "general" - return cell.Value, nil + return cellValue, nil case builtInNumFmt[builtInNumFmtIndex_STRING]: // String is "@" - return textFormat.prefix + cell.Value + textFormat.suffix, nil + return textFormat.prefix + cellValue + textFormat.suffix, nil case "": // If cell is not "General" and there is not an "@" symbol in the format, then the cell's value is not // used when determining what to display. It would be completely legal to have a format of "Error" @@ -99,7 +105,7 @@ func (fullFormat *parsedNumberFormat) FormatValue(cell *Cell) (string, error) { // have a prefix of "Error" and a reduced format string of "" (empty string). return textFormat.prefix + textFormat.suffix, nil default: - return cell.Value, errors.New("invalid or unsupported format, unsupported string format") + return cellValue, errors.New("invalid or unsupported format, unsupported string format") } case CellTypeDate: // These are dates that are stored in date format instead of being stored as numbers with a format to turn them diff --git a/lib.go b/lib.go index 96e7a062..f8cfa4d3 100644 --- a/lib.go +++ b/lib.go @@ -464,7 +464,7 @@ func fillCellData(rawCell xlsxC, refTable *RefTable, sharedFormulas map[int]shar if err != nil { panic(err) } - cell.Value = refTable.ResolveSharedString(ref) + cell.Value, cell.RichText = refTable.ResolveSharedString(ref) } case "inlineStr": cell.cellType = CellTypeInline @@ -496,13 +496,12 @@ func fillCellData(rawCell xlsxC, refTable *RefTable, sharedFormulas map[int]shar // fillCellDataFromInlineString attempts to get inline string data and put it into a Cell. func fillCellDataFromInlineString(rawcell xlsxC, cell *Cell) { cell.Value = "" + cell.RichText = nil if rawcell.Is != nil { - if rawcell.Is.T != "" { - cell.Value = strings.Trim(rawcell.Is.T, " \t\n\r") + if rawcell.Is.T != nil { + cell.Value = strings.Trim(rawcell.Is.T.getText(), " \t\n\r") } else { - for _, r := range rawcell.Is.R { - cell.Value += r.T - } + cell.RichText = xmlToRichText(rawcell.Is.R) } } } diff --git a/reftable.go b/reftable.go index f4f6bc03..fb711b26 100644 --- a/reftable.go +++ b/reftable.go @@ -1,19 +1,27 @@ package xlsx +type plainTextOrRichText struct { + plainText string + isRichText bool + richText []RichTextRun +} + type RefTable struct { - indexedStrings []string + indexedStrings []plainTextOrRichText knownStrings map[string]int + knownRichTexts map[string][]int isWrite bool } -// NewSharedStringRefTable() creates a new, empty RefTable. +// NewSharedStringRefTable creates a new, empty RefTable. func NewSharedStringRefTable() *RefTable { rt := RefTable{} rt.knownStrings = make(map[string]int) + rt.knownRichTexts = make(map[string][]int) return &rt } -// MakeSharedStringRefTable() takes an xlsxSST struct and converts +// MakeSharedStringRefTable takes an xlsxSST struct and converts // it's contents to an slice of strings used to refer to string values // by numeric index - this is the model used within XLSX worksheet (a // numeric reference is stored to a shared cell value). @@ -22,19 +30,16 @@ func MakeSharedStringRefTable(source *xlsxSST) *RefTable { reftable.isWrite = false for _, si := range source.SI { if len(si.R) > 0 { - newString := "" - for j := 0; j < len(si.R); j++ { - newString = newString + si.R[j].T - } - reftable.AddString(newString) + richText := xmlToRichText(si.R) + reftable.AddRichText(richText) } else { - reftable.AddString(si.T) + reftable.AddString(si.T.getText()) } } return reftable } -// makeXlsxSST() takes a RefTable and returns and +// makeXlsxSST takes a RefTable and returns and // equivalent xlsxSST representation. func (rt *RefTable) makeXLSXSST() xlsxSST { sst := xlsxSST{} @@ -42,18 +47,28 @@ func (rt *RefTable) makeXLSXSST() xlsxSST { sst.UniqueCount = sst.Count for _, ref := range rt.indexedStrings { si := xlsxSI{} - si.T = ref + if ref.isRichText { + si.R = richTextToXml(ref.richText) + } else { + si.T = &xlsxT{Text: ref.plainText} + } sst.SI = append(sst.SI, si) } return sst } -// Resolvesharedstring() looks up a string value by numeric index from -// a provided reference table (just a slice of strings in the correct -// order). This function only exists to provide clarity of purpose -// via it's name. -func (rt *RefTable) ResolveSharedString(index int) string { - return rt.indexedStrings[index] +// ResolveSharedString looks up a string value or the rich text by numeric index from +// a provided reference table (just a slice of strings in the correct order). +// If the rich text was found, non-empty slice will be returned in richText. +// This function only exists to provide clarity of purpose via it's name. +func (rt *RefTable) ResolveSharedString(index int) (plainText string, richText []RichTextRun) { + ptrt := rt.indexedStrings[index] + if ptrt.isRichText { + richText = ptrt.richText + } else { + plainText = ptrt.plainText + } + return } // AddString adds a string to the reference table and return it's @@ -66,12 +81,49 @@ func (rt *RefTable) AddString(str string) int { return index } } - rt.indexedStrings = append(rt.indexedStrings, str) + ptrt := plainTextOrRichText{plainText: str, isRichText: false} + rt.indexedStrings = append(rt.indexedStrings, ptrt) index := len(rt.indexedStrings) - 1 rt.knownStrings[str] = index return index } +// AddRichText adds a set of rich text to the reference table and return it's +// numeric index. If a set of rich text already exists then it simply returns +// the existing index. +func (rt *RefTable) AddRichText(r []RichTextRun) int { + plain := richTextToPlainText(r) + if rt.isWrite { + indices, ok := rt.knownRichTexts[plain] + if ok { + for _, index := range indices { + if areRichTextsEqual(rt.indexedStrings[index].richText, r) { + return index + } + } + } + } + ptrt := plainTextOrRichText{isRichText: true} + ptrt.richText = append(ptrt.richText, r...) + rt.indexedStrings = append(rt.indexedStrings, ptrt) + index := len(rt.indexedStrings) - 1 + rt.knownRichTexts[plain] = append(rt.knownRichTexts[plain], index) + return index +} + +func areRichTextsEqual(r1 []RichTextRun, r2 []RichTextRun) bool { + if len(r1) != len(r2) { + return false + } + for i, rt1 := range r1 { + rt2 := r2[i] + if !rt1.Equals(&rt2) { + return false + } + } + return true +} + func (rt *RefTable) Length() int { return len(rt.indexedStrings) } diff --git a/reftable_test.go b/reftable_test.go index cde4d39d..d1cda087 100644 --- a/reftable_test.go +++ b/reftable_test.go @@ -26,12 +26,29 @@ func (s *RefTableSuite) SetUpTest(c *C) { Bar - Baz + Baz + Quuk - `) + + + + + + + Text1 + + + + + + + Text2 + + + `) } // We can add a new string to the RefTable @@ -39,15 +56,21 @@ func (s *RefTableSuite) TestRefTableAddString(c *C) { refTable := NewSharedStringRefTable() index := refTable.AddString("Foo") c.Assert(index, Equals, 0) - c.Assert(refTable.ResolveSharedString(0), Equals, "Foo") + p, r := refTable.ResolveSharedString(0) + c.Assert(p, Equals, "Foo") + c.Assert(r, IsNil) } func (s *RefTableSuite) TestCreateNewSharedStringRefTable(c *C) { refTable := NewSharedStringRefTable() refTable.AddString("Foo") refTable.AddString("Bar") - c.Assert(refTable.ResolveSharedString(0), Equals, "Foo") - c.Assert(refTable.ResolveSharedString(1), Equals, "Bar") + p, r := refTable.ResolveSharedString(0) + c.Assert(p, Equals, "Foo") + c.Assert(r, IsNil) + p, r = refTable.ResolveSharedString(1) + c.Assert(p, Equals, "Bar") + c.Assert(r, IsNil) } // Test we can correctly convert a xlsxSST into a reference table @@ -57,9 +80,26 @@ func (s *RefTableSuite) TestMakeSharedStringRefTable(c *C) { err := xml.NewDecoder(s.SharedStringsXML).Decode(sst) c.Assert(err, IsNil) reftable := MakeSharedStringRefTable(sst) - c.Assert(reftable.Length(), Equals, 4) - c.Assert(reftable.ResolveSharedString(0), Equals, "Foo") - c.Assert(reftable.ResolveSharedString(1), Equals, "Bar") + c.Assert(reftable.Length(), Equals, 5) + p, r := reftable.ResolveSharedString(0) + c.Assert(p, Equals, "Foo") + c.Assert(r, IsNil) + p, r = reftable.ResolveSharedString(1) + c.Assert(p, Equals, "Bar") + c.Assert(r, IsNil) + p, r = reftable.ResolveSharedString(2) + c.Assert(p, Equals, "Baz \n") + c.Assert(r, IsNil) + p, r = reftable.ResolveSharedString(3) + c.Assert(p, Equals, "Quuk") + c.Assert(r, IsNil) + p, r = reftable.ResolveSharedString(4) + c.Assert(p, Equals, "") + c.Assert(r, HasLen, 2) + c.Assert(r[0].Font.Size, Equals, 11.5) + c.Assert(r[0].Font.Name, Equals, "Font1") + c.Assert(r[1].Font.Size, Equals, 12.5) + c.Assert(r[1].Font.Name, Equals, "Font2") } // Test we can correctly resolve a numeric reference in the reference @@ -69,7 +109,9 @@ func (s *RefTableSuite) TestResolveSharedString(c *C) { err := xml.NewDecoder(s.SharedStringsXML).Decode(sst) c.Assert(err, IsNil) reftable := MakeSharedStringRefTable(sst) - c.Assert(reftable.ResolveSharedString(0), Equals, "Foo") + p, r := reftable.ResolveSharedString(0) + c.Assert(p, Equals, "Foo") + c.Assert(r, IsNil) } // Test we can correctly create the xlsx.xlsxSST struct from a RefTable @@ -77,18 +119,52 @@ func (s *RefTableSuite) TestMakeXLSXSST(c *C) { refTable := NewSharedStringRefTable() refTable.AddString("Foo") refTable.AddString("Bar") + refTable.AddRichText([]RichTextRun{ + RichTextRun{ + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Bold: true, + }, + Text: "Text1", + }, + RichTextRun{ + Text: "Text2", + }, + }) sst := refTable.makeXLSXSST() c.Assert(sst, NotNil) - c.Assert(sst.Count, Equals, 2) - c.Assert(sst.UniqueCount, Equals, 2) - c.Assert(sst.SI, HasLen, 2) + c.Assert(sst.Count, Equals, 3) + c.Assert(sst.UniqueCount, Equals, 3) + c.Assert(sst.SI, HasLen, 3) si := sst.SI[0] - c.Assert(si.T, Equals, "Foo") + c.Assert(si.T.Text, Equals, "Foo") + c.Assert(si.R, IsNil) + si = sst.SI[2] + c.Assert(si.T, IsNil) + c.Assert(si.R, HasLen, 2) + c.Assert(si.R[0].RPr.B, NotNil) + c.Assert(si.R[0].T.Text, Equals, "Text1") + c.Assert(si.R[1].RPr, IsNil) + c.Assert(si.R[1].T.Text, Equals, "Text2") } func (s *RefTableSuite) TestMarshalSST(c *C) { refTable := NewSharedStringRefTable() refTable.AddString("Foo") + refTable.AddRichText([]RichTextRun{ + RichTextRun{ + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Bold: true, + }, + Text: "Text1", + }, + RichTextRun{ + Text: "Text2", + }, + }) sst := refTable.makeXLSXSST() output := bytes.NewBufferString(xml.Header) @@ -99,7 +175,7 @@ func (s *RefTableSuite) TestMarshalSST(c *C) { c.Assert(err, IsNil) expectedXLSXSST := ` -Foo` +FooText1Text2` c.Assert(output.String(), Equals, expectedXLSXSST) } @@ -110,8 +186,12 @@ func (s *RefTableSuite) TestRefTableReadAddString(c *C) { index2 := refTable.AddString("Foo") c.Assert(index1, Equals, 0) c.Assert(index2, Equals, 1) - c.Assert(refTable.ResolveSharedString(0), Equals, "Foo") - c.Assert(refTable.ResolveSharedString(1), Equals, "Foo") + p, r := refTable.ResolveSharedString(0) + c.Assert(p, Equals, "Foo") + c.Assert(r, IsNil) + p, r = refTable.ResolveSharedString(1) + c.Assert(p, Equals, "Foo") + c.Assert(r, IsNil) } func (s *RefTableSuite) TestRefTableWriteAddString(c *C) { @@ -121,5 +201,78 @@ func (s *RefTableSuite) TestRefTableWriteAddString(c *C) { index2 := refTable.AddString("Foo") c.Assert(index1, Equals, 0) c.Assert(index2, Equals, 0) - c.Assert(refTable.ResolveSharedString(0), Equals, "Foo") + p, r := refTable.ResolveSharedString(0) + c.Assert(p, Equals, "Foo") + c.Assert(r, IsNil) +} + +func (s *RefTableSuite) TestRefTableReadAddRichText(c *C) { + refTable := NewSharedStringRefTable() + refTable.isWrite = false + index1 := refTable.AddRichText([]RichTextRun{ + RichTextRun{ + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Bold: true, + }, + Text: "Text1", + }, + }) + index2 := refTable.AddRichText([]RichTextRun{ + RichTextRun{ + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Bold: true, + }, + Text: "Text1", + }, + }) + + c.Assert(index1, Equals, 0) + c.Assert(index2, Equals, 1) + p, r := refTable.ResolveSharedString(0) + c.Assert(p, Equals, "") + c.Assert(r, HasLen, 1) + c.Assert(r[0].Font.Bold, NotNil) + c.Assert(r[0].Text, Equals, "Text1") + p, r = refTable.ResolveSharedString(1) + c.Assert(p, Equals, "") + c.Assert(r, HasLen, 1) + c.Assert(r[0].Font.Bold, NotNil) + c.Assert(r[0].Text, Equals, "Text1") +} + +func (s *RefTableSuite) TestRefTableWriteAddRichText(c *C) { + refTable := NewSharedStringRefTable() + refTable.isWrite = true + index1 := refTable.AddRichText([]RichTextRun{ + RichTextRun{ + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Bold: true, + }, + Text: "Text1", + }, + }) + index2 := refTable.AddRichText([]RichTextRun{ + RichTextRun{ + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Bold: true, + }, + Text: "Text1", + }, + }) + + c.Assert(index1, Equals, 0) + c.Assert(index2, Equals, 0) + p, r := refTable.ResolveSharedString(0) + c.Assert(p, Equals, "") + c.Assert(r, HasLen, 1) + c.Assert(r[0].Font.Bold, NotNil) + c.Assert(r[0].Text, Equals, "Text1") } diff --git a/richtext.go b/richtext.go new file mode 100644 index 00000000..95c06049 --- /dev/null +++ b/richtext.go @@ -0,0 +1,207 @@ +package xlsx + +import ( + "fmt" + "reflect" +) + +type RichTextFontFamily int +type RichTextCharset int +type RichTextVertAlign string +type RichTextUnderline string + +const ( + // RichTextFontFamilyUnspecified indicates that the font family was not specified + RichTextFontFamilyUnspecified RichTextFontFamily = -1 + RichTextFontFamilyNotApplicable RichTextFontFamily = 0 + RichTextFontFamilyRoman RichTextFontFamily = 1 + RichTextFontFamilySwiss RichTextFontFamily = 2 + RichTextFontFamilyModern RichTextFontFamily = 3 + RichTextFontFamilyScript RichTextFontFamily = 4 + RichTextFontFamilyDecorative RichTextFontFamily = 5 + + // RichTextCharsetUnspecified indicates that the font charset was not specified + RichTextCharsetUnspecified RichTextCharset = -1 + RichTextCharsetANSI RichTextCharset = 0 + RichTextCharsetDefault RichTextCharset = 1 + RichTextCharsetSymbol RichTextCharset = 2 + RichTextCharsetMac RichTextCharset = 77 + RichTextCharsetShiftJIS RichTextCharset = 128 + RichTextCharsetHangul RichTextCharset = 129 + RichTextCharsetJohab RichTextCharset = 130 + RichTextCharsetGB2312 RichTextCharset = 134 + RichTextCharsetBIG5 RichTextCharset = 136 + RichTextCharsetGreek RichTextCharset = 161 + RichTextCharsetTurkish RichTextCharset = 162 + RichTextCharsetVietnamese RichTextCharset = 163 + RichTextCharsetHebrew RichTextCharset = 177 + RichTextCharsetArabic RichTextCharset = 178 + RichTextCharsetBaltic RichTextCharset = 186 + RichTextCharsetRussian RichTextCharset = 204 + RichTextCharsetThai RichTextCharset = 222 + RichTextCharsetEastEurope RichTextCharset = 238 + RichTextCharsetOEM RichTextCharset = 255 + + RichTextVertAlignSuperscript RichTextVertAlign = "superscript" + RichTextVertAlignSubscript RichTextVertAlign = "subscript" + + RichTextUnderlineSingle RichTextUnderline = "single" + RichTextUnderlineDouble RichTextUnderline = "double" + + // These underline styles doesn't work on the RichTextRun, + // and should be set as a part of cell style. + // "singleAccounting" + // "doubleAccounting" +) + +// RichTextColor is the color of the RichTextRun. +type RichTextColor struct { + coreColor xlsxColor +} + +// NewRichTextColorFromARGB creates a new RichTextColor from ARGB component values. +// Each component must have a value in range of 0 to 255. +func NewRichTextColorFromARGB(alpha, red, green, blue int) *RichTextColor { + argb := fmt.Sprintf("%02X%02X%02X%02X", alpha, red, green, blue) + return &RichTextColor{coreColor: xlsxColor{RGB: argb}} +} + +// NewRichTextColorFromThemeColor creates a new RichTextColor from the theme color. +// The argument `themeColor` is a zero-based index of the theme color. +func NewRichTextColorFromThemeColor(themeColor int) *RichTextColor { + return &RichTextColor{coreColor: xlsxColor{Theme: &themeColor}} +} + +// RichTextFont is the font spec of the RichTextRun. +type RichTextFont struct { + // Name is the font name. If Name is empty, Size, Family and Charset will be ignored. + Name string + // Size is the font size. + Size float64 + // Family is a value of the font family. Use one of the RichTextFontFamily constants. + Family RichTextFontFamily + // Charset is a value of the charset of the font. Use one of the RichTextCharset constants. + Charset RichTextCharset + // Color is the text color. + Color *RichTextColor + // Bold specifies the bold face font style. + Bold bool + // Italic specifies the italic font style. + Italic bool + // Strike specifies a strikethrough line. + Strike bool + // VertAlign specifies the vertical position of the text. Use one of the RichTextVertAlign constants, or empty. + VertAlign RichTextVertAlign + // Underline specifies the underline style. Use one of the RichTextUnderline constants, or empty. + Underline RichTextUnderline +} + +// RichTextRun is a run of the decorated text. +type RichTextRun struct { + Font *RichTextFont + Text string +} + +func (rt *RichTextRun) Equals(other *RichTextRun) bool { + return reflect.DeepEqual(rt, other) +} + +func richTextToXml(r []RichTextRun) []xlsxR { + var xrs []xlsxR + for _, rt := range r { + xr := xlsxR{} + xr.T = xlsxT{Text: rt.Text} + if rt.Font != nil { + rpr := xlsxRunProperties{} + if len(rt.Font.Name) > 0 { + rpr.RFont = &xlsxVal{Val: rt.Font.Name} + } + if rt.Font.Size > 0.0 { + rpr.Sz = &xlsxFloatVal{Val: rt.Font.Size} + } + if rt.Font.Family != RichTextFontFamilyUnspecified { + rpr.Family = &xlsxIntVal{Val: int(rt.Font.Family)} + } + if rt.Font.Charset != RichTextCharsetUnspecified { + rpr.Charset = &xlsxIntVal{Val: int(rt.Font.Charset)} + } + if rt.Font.Color != nil { + xcolor := rt.Font.Color.coreColor + rpr.Color = &xcolor + } + if rt.Font.Bold { + rpr.B.Val = true + } + if rt.Font.Italic { + rpr.I.Val = true + } + if rt.Font.Strike { + rpr.Strike.Val = true + } + if len(rt.Font.VertAlign) > 0 { + rpr.VertAlign = &xlsxVal{Val: string(rt.Font.VertAlign)} + } + if len(rt.Font.Underline) > 0 { + rpr.U = &xlsxVal{Val: string(rt.Font.Underline)} + } + xr.RPr = &rpr + } + xrs = append(xrs, xr) + } + return xrs +} + +func xmlToRichText(r []xlsxR) []RichTextRun { + richiText := []RichTextRun(nil) + for _, rr := range r { + rtr := RichTextRun{Text: rr.T.Text} + rpr := rr.RPr + if rpr != nil { + rtr.Font = &RichTextFont{} + if rpr.RFont != nil { + rtr.Font.Name = rpr.RFont.Val + } + if rpr.Sz != nil { + rtr.Font.Size = rpr.Sz.Val + } + if rpr.Family != nil { + rtr.Font.Family = RichTextFontFamily(rpr.Family.Val) + } else { + rtr.Font.Family = RichTextFontFamilyUnspecified + } + if rpr.Charset != nil { + rtr.Font.Charset = RichTextCharset(rpr.Charset.Val) + } else { + rtr.Font.Charset = RichTextCharsetUnspecified + } + if rpr.Color != nil { + rtr.Font.Color = &RichTextColor{coreColor: *rpr.Color} + } + if rpr.B.Val { + rtr.Font.Bold = true + } + if rpr.I.Val { + rtr.Font.Italic = true + } + if rpr.Strike.Val { + rtr.Font.Strike = true + } + if rpr.VertAlign != nil { + rtr.Font.VertAlign = RichTextVertAlign(rpr.VertAlign.Val) + } + if rpr.U != nil { + rtr.Font.Underline = RichTextUnderline(rpr.U.Val) + } + } + richiText = append(richiText, rtr) + } + return richiText +} + +func richTextToPlainText(richText []RichTextRun) string { + var s string + for _, r := range richText { + s += r.Text + } + return s +} diff --git a/richtext_test.go b/richtext_test.go new file mode 100644 index 00000000..977f7ab9 --- /dev/null +++ b/richtext_test.go @@ -0,0 +1,388 @@ +package xlsx + +import ( + . "gopkg.in/check.v1" +) + +type RichTextSuite struct{} + +var _ = Suite(&RichTextSuite{}) + +func (s *RichTextSuite) TestNewRichTextColorFromARGB(c *C) { + rtColor := NewRichTextColorFromARGB(127, 128, 129, 130) + c.Assert(rtColor.coreColor.RGB, Equals, "7F808182") +} + +func (s *RichTextSuite) TestNewRichTextColorFromThemeColor(c *C) { + rtColor := NewRichTextColorFromThemeColor(123) + c.Assert(*rtColor.coreColor.Theme, Equals, 123) +} + +func (s *RichTextSuite) TestRichTextRunEquals(c *C) { + r1color := 1 + r1 := &RichTextRun{ + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Color: &RichTextColor{coreColor: xlsxColor{Theme: &r1color}}, + Bold: true, + Italic: true, + }, + Text: "X", + } + + r2color := r1color + r2 := &RichTextRun{ // same with r1 + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Color: &RichTextColor{coreColor: xlsxColor{Theme: &r2color}}, + Bold: true, + Italic: true, + }, + Text: "X", + } + + r3color := r1color + r3 := &RichTextRun{ // different font setting from r1 + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Color: &RichTextColor{coreColor: xlsxColor{Theme: &r3color}}, + Bold: true, + Italic: false, + }, + Text: "X", + } + + r4color := 2 + r4 := &RichTextRun{ // different color setting from r1 + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Color: &RichTextColor{coreColor: xlsxColor{Theme: &r4color}}, + Bold: true, + Italic: true, + }, + Text: "X", + } + + r5 := &RichTextRun{ // no font setting + Text: "X", + } + + r6color := r1color + r6 := &RichTextRun{ // different text from r1 + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Color: &RichTextColor{coreColor: xlsxColor{Theme: &r6color}}, + Bold: true, + Italic: true, + }, + Text: "Y", + } + + var r7 *RichTextRun = nil + + c.Assert(r1.Equals(r2), Equals, true) + c.Assert(r1.Equals(r3), Equals, false) + c.Assert(r1.Equals(r4), Equals, false) + c.Assert(r1.Equals(r5), Equals, false) + c.Assert(r1.Equals(r6), Equals, false) + c.Assert(r1.Equals(r7), Equals, false) + + c.Assert(r2.Equals(r1), Equals, true) + c.Assert(r3.Equals(r1), Equals, false) + c.Assert(r4.Equals(r1), Equals, false) + c.Assert(r5.Equals(r1), Equals, false) + c.Assert(r6.Equals(r1), Equals, false) + c.Assert(r7.Equals(r1), Equals, false) + + c.Assert(r7.Equals(nil), Equals, true) +} + +func (s *RichTextSuite) TestRichTextToXml(c *C) { + rtr := []RichTextRun{ + RichTextRun{ + Font: &RichTextFont{ + Name: "Font", + Size: 12.345, + Family: RichTextFontFamilyScript, + Charset: RichTextCharsetHebrew, + Color: &RichTextColor{coreColor: xlsxColor{RGB: "DEADBEEF"}}, + Bold: true, + Italic: false, + Strike: false, + VertAlign: RichTextVertAlignSuperscript, + Underline: RichTextUnderlineSingle, + }, + Text: "Bold", + }, + RichTextRun{ + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Italic: true, + }, + Text: "Italic", + }, + RichTextRun{ + Font: &RichTextFont{ + Family: RichTextFontFamilyUnspecified, + Charset: RichTextCharsetUnspecified, + Strike: true, + }, + Text: "Strike", + }, + RichTextRun{ + Font: &RichTextFont{}, + Text: "Empty", + }, + RichTextRun{ + Text: "No Font", + }, + } + + xmlr := richTextToXml(rtr) + c.Assert(xmlr, HasLen, 5) + + r := xmlr[0] + c.Assert(r.RPr.RFont.Val, Equals, "Font") + c.Assert(r.RPr.Charset.Val, Equals, int(RichTextCharsetHebrew)) + c.Assert(r.RPr.Family.Val, Equals, int(RichTextFontFamilyScript)) + c.Assert(r.RPr.B.Val, Equals, true) + c.Assert(r.RPr.I.Val, Equals, false) + c.Assert(r.RPr.Strike.Val, Equals, false) + c.Assert(r.RPr.Outline.Val, Equals, false) + c.Assert(r.RPr.Shadow.Val, Equals, false) + c.Assert(r.RPr.Condense.Val, Equals, false) + c.Assert(r.RPr.Extend.Val, Equals, false) + c.Assert(r.RPr.Color.RGB, Equals, "DEADBEEF") + c.Assert(r.RPr.Sz.Val, Equals, 12.345) + c.Assert(r.RPr.U.Val, Equals, string(RichTextUnderlineSingle)) + c.Assert(r.RPr.VertAlign.Val, Equals, string(RichTextVertAlignSuperscript)) + c.Assert(r.RPr.Scheme, IsNil) + c.Assert(r.T.Text, Equals, "Bold") + + r = xmlr[1] + c.Assert(r.RPr.RFont, IsNil) + c.Assert(r.RPr.Charset, IsNil) + c.Assert(r.RPr.Family, IsNil) + c.Assert(r.RPr.B.Val, Equals, false) + c.Assert(r.RPr.I.Val, Equals, true) + c.Assert(r.RPr.Strike.Val, Equals, false) + c.Assert(r.RPr.Outline.Val, Equals, false) + c.Assert(r.RPr.Shadow.Val, Equals, false) + c.Assert(r.RPr.Condense.Val, Equals, false) + c.Assert(r.RPr.Extend.Val, Equals, false) + c.Assert(r.RPr.Color, IsNil) + c.Assert(r.RPr.Sz, IsNil) + c.Assert(r.RPr.U, IsNil) + c.Assert(r.RPr.VertAlign, IsNil) + c.Assert(r.RPr.Scheme, IsNil) + c.Assert(r.T.Text, Equals, "Italic") + + r = xmlr[2] + c.Assert(r.RPr.RFont, IsNil) + c.Assert(r.RPr.Charset, IsNil) + c.Assert(r.RPr.Family, IsNil) + c.Assert(r.RPr.B.Val, Equals, false) + c.Assert(r.RPr.I.Val, Equals, false) + c.Assert(r.RPr.Strike.Val, Equals, true) + c.Assert(r.RPr.Outline.Val, Equals, false) + c.Assert(r.RPr.Shadow.Val, Equals, false) + c.Assert(r.RPr.Condense.Val, Equals, false) + c.Assert(r.RPr.Extend.Val, Equals, false) + c.Assert(r.RPr.Color, IsNil) + c.Assert(r.RPr.Sz, IsNil) + c.Assert(r.RPr.U, IsNil) + c.Assert(r.RPr.VertAlign, IsNil) + c.Assert(r.RPr.Scheme, IsNil) + c.Assert(r.T.Text, Equals, "Strike") + + r = xmlr[3] + c.Assert(r.RPr.RFont, IsNil) + c.Assert(r.RPr.Charset.Val, Equals, int(RichTextCharsetANSI)) + c.Assert(r.RPr.Family.Val, Equals, int(RichTextFontFamilyNotApplicable)) + c.Assert(r.RPr.B.Val, Equals, false) + c.Assert(r.RPr.I.Val, Equals, false) + c.Assert(r.RPr.Strike.Val, Equals, false) + c.Assert(r.RPr.Outline.Val, Equals, false) + c.Assert(r.RPr.Shadow.Val, Equals, false) + c.Assert(r.RPr.Condense.Val, Equals, false) + c.Assert(r.RPr.Extend.Val, Equals, false) + c.Assert(r.RPr.Color, IsNil) + c.Assert(r.RPr.Sz, IsNil) + c.Assert(r.RPr.U, IsNil) + c.Assert(r.RPr.VertAlign, IsNil) + c.Assert(r.RPr.Scheme, IsNil) + c.Assert(r.T.Text, Equals, "Empty") + + r = xmlr[4] + c.Assert(r.RPr, IsNil) + c.Assert(r.T.Text, Equals, "No Font") +} + +func (s *RichTextSuite) TestXmlToRichText(c *C) { + xmlr := []xlsxR{ + xlsxR{ + RPr: &xlsxRunProperties{ + RFont: &xlsxVal{Val: "Font"}, + Charset: &xlsxIntVal{Val: int(RichTextCharsetGreek)}, + Family: &xlsxIntVal{Val: int(RichTextFontFamilySwiss)}, + B: xlsxBoolProp{Val: true}, + I: xlsxBoolProp{Val: false}, + Strike: xlsxBoolProp{Val: false}, + Outline: xlsxBoolProp{Val: false}, + Shadow: xlsxBoolProp{Val: false}, + Condense: xlsxBoolProp{Val: false}, + Extend: xlsxBoolProp{Val: false}, + Color: &xlsxColor{RGB: "DEADBEEF"}, + Sz: &xlsxFloatVal{Val: 12.345}, + U: &xlsxVal{Val: string(RichTextUnderlineDouble)}, + VertAlign: &xlsxVal{Val: string(RichTextVertAlignSuperscript)}, + Scheme: nil, + }, + T: xlsxT{Text: "Bold"}, + }, + xlsxR{ + RPr: &xlsxRunProperties{ + RFont: nil, + Charset: nil, + Family: nil, + B: xlsxBoolProp{Val: false}, + I: xlsxBoolProp{Val: true}, + Strike: xlsxBoolProp{Val: false}, + Outline: xlsxBoolProp{Val: false}, + Shadow: xlsxBoolProp{Val: false}, + Condense: xlsxBoolProp{Val: false}, + Extend: xlsxBoolProp{Val: false}, + Color: nil, + Sz: nil, + U: nil, + VertAlign: nil, + Scheme: nil, + }, + T: xlsxT{Text: "Italic"}, + }, + xlsxR{ + RPr: &xlsxRunProperties{ + RFont: nil, + Charset: nil, + Family: nil, + B: xlsxBoolProp{Val: false}, + I: xlsxBoolProp{Val: false}, + Strike: xlsxBoolProp{Val: true}, + Outline: xlsxBoolProp{Val: false}, + Shadow: xlsxBoolProp{Val: false}, + Condense: xlsxBoolProp{Val: false}, + Extend: xlsxBoolProp{Val: false}, + Color: nil, + Sz: nil, + U: nil, + VertAlign: nil, + Scheme: nil, + }, + T: xlsxT{Text: "Strike"}, + }, + xlsxR{ + RPr: &xlsxRunProperties{}, + T: xlsxT{Text: "Empty"}, + }, + xlsxR{ + RPr: nil, + T: xlsxT{Text: "No Font"}, + }, + } + + rtr := xmlToRichText(xmlr) + c.Assert(rtr, HasLen, 5) + + r := rtr[0] + c.Assert(r.Font.Name, Equals, "Font") + c.Assert(r.Font.Size, Equals, 12.345) + c.Assert(r.Font.Family, Equals, RichTextFontFamilySwiss) + c.Assert(r.Font.Charset, Equals, RichTextCharsetGreek) + c.Assert(r.Font.Color.coreColor.RGB, Equals, "DEADBEEF") + c.Assert(r.Font.Bold, Equals, true) + c.Assert(r.Font.Italic, Equals, false) + c.Assert(r.Font.Strike, Equals, false) + c.Assert(r.Font.VertAlign, Equals, RichTextVertAlignSuperscript) + c.Assert(r.Font.Underline, Equals, RichTextUnderlineDouble) + c.Assert(r.Text, Equals, "Bold") + + r = rtr[1] + c.Assert(r.Font.Name, Equals, "") + c.Assert(r.Font.Size, Equals, 0.0) + c.Assert(r.Font.Family, Equals, RichTextFontFamilyUnspecified) + c.Assert(r.Font.Charset, Equals, RichTextCharsetUnspecified) + c.Assert(r.Font.Color, IsNil) + c.Assert(r.Font.Bold, Equals, false) + c.Assert(r.Font.Italic, Equals, true) + c.Assert(r.Font.Strike, Equals, false) + c.Assert(r.Font.VertAlign, Equals, RichTextVertAlign("")) + c.Assert(r.Font.Underline, Equals, RichTextUnderline("")) + c.Assert(r.Text, Equals, "Italic") + + r = rtr[2] + c.Assert(r.Font.Name, Equals, "") + c.Assert(r.Font.Size, Equals, 0.0) + c.Assert(r.Font.Family, Equals, RichTextFontFamilyUnspecified) + c.Assert(r.Font.Charset, Equals, RichTextCharsetUnspecified) + c.Assert(r.Font.Color, IsNil) + c.Assert(r.Font.Bold, Equals, false) + c.Assert(r.Font.Italic, Equals, false) + c.Assert(r.Font.Strike, Equals, true) + c.Assert(r.Font.VertAlign, Equals, RichTextVertAlign("")) + c.Assert(r.Font.Underline, Equals, RichTextUnderline("")) + c.Assert(r.Text, Equals, "Strike") + + r = rtr[3] + c.Assert(r.Font.Name, Equals, "") + c.Assert(r.Font.Size, Equals, 0.0) + c.Assert(r.Font.Family, Equals, RichTextFontFamilyUnspecified) + c.Assert(r.Font.Charset, Equals, RichTextCharsetUnspecified) + c.Assert(r.Font.Color, IsNil) + c.Assert(r.Font.Bold, Equals, false) + c.Assert(r.Font.Italic, Equals, false) + c.Assert(r.Font.Strike, Equals, false) + c.Assert(r.Font.VertAlign, Equals, RichTextVertAlign("")) + c.Assert(r.Font.Underline, Equals, RichTextUnderline("")) + c.Assert(r.Text, Equals, "Empty") + + r = rtr[4] + c.Assert(r.Font, IsNil) + c.Assert(r.Text, Equals, "No Font") +} + +func (s *RichTextSuite) TestRichTextToPlainText(c *C) { + rt := []RichTextRun{ + RichTextRun{ + Font: &RichTextFont{ + Bold: true, + }, + Text: "Bold", + }, + RichTextRun{ + Font: &RichTextFont{ + Italic: true, + }, + Text: "Italic", + }, + RichTextRun{ + Font: &RichTextFont{ + Strike: true, + }, + Text: "Strike", + }, + } + plainText := richTextToPlainText(rt) + c.Assert(plainText, Equals, "BoldItalicStrike") +} + +func (s *RichTextSuite) TestRichTextToPlainTextEmpty(c *C) { + rt := []RichTextRun{} + plainText := richTextToPlainText(rt) + c.Assert(plainText, Equals, "") +} diff --git a/sheet.go b/sheet.go index bbdc350c..9293aed4 100644 --- a/sheet.go +++ b/sheet.go @@ -546,6 +546,8 @@ func (s *Sheet) makeRows(worksheet *xlsxWorksheet, styles *xlsxStyleSheet, refTa case CellTypeString: if len(cell.Value) > 0 { xC.V = strconv.Itoa(refTable.AddString(cell.Value)) + } else if len(cell.RichText) > 0 { + xC.V = strconv.Itoa(refTable.AddRichText(cell.RichText)) } xC.T = "s" case CellTypeNumeric: diff --git a/stream_file.go b/stream_file.go index 9ad701fb..ea542105 100644 --- a/stream_file.go +++ b/stream_file.go @@ -318,12 +318,12 @@ func makeXlsxCell(cellType CellType, cellCoordinate string, cellStyleId int, cel case CellTypeError: return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "e", V: cellData}, nil case CellTypeInline: - return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "inlineStr", Is: &xlsxSI{T: cellData}}, nil + return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "inlineStr", Is: &xlsxSI{T: &xlsxT{Text: cellData}}}, nil case CellTypeNumeric: return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "n", V: cellData}, nil case CellTypeString: // TODO Currently shared strings are types as inline strings - return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "inlineStr", Is: &xlsxSI{T: cellData}}, nil + return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "inlineStr", Is: &xlsxSI{T: &xlsxT{Text: cellData}}}, nil // TODO currently not supported // case CellTypeStringFormula: // return xlsxC{}, UnsupportedCellTypeError diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 306a8020..9a614da2 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -2,6 +2,8 @@ package xlsx import ( "encoding/xml" + "errors" + "strings" ) // xlsxSST directly maps the sst element from the namespace @@ -19,7 +21,7 @@ type xlsxSST struct { // currently I have not checked this for completeness - it does as // much as I need. type xlsxSI struct { - T string `xml:"t"` + T *xlsxT `xml:"t"` R []xlsxR `xml:"r"` } @@ -28,5 +30,135 @@ type xlsxSI struct { // currently I have not checked this for completeness - it does as // much as I need. type xlsxR struct { - T string `xml:"t"` + RPr *xlsxRunProperties `xml:"rPr"` + T xlsxT `xml:"t"` +} + +// xlsxRunProperties directly maps the rPr element from the namespace +// http://schemas.openxmlformats.org/spreadsheetml/2006/main +type xlsxRunProperties struct { + RFont *xlsxVal `xml:"rFont"` + Charset *xlsxIntVal `xml:"charset"` + Family *xlsxIntVal `xml:"family"` + B xlsxBoolProp `xml:"b"` + I xlsxBoolProp `xml:"i"` + Strike xlsxBoolProp `xml:"strike"` + Outline xlsxBoolProp `xml:"outline"` + Shadow xlsxBoolProp `xml:"shadow"` + Condense xlsxBoolProp `xml:"condense"` + Extend xlsxBoolProp `xml:"extend"` + Color *xlsxColor `xml:"color"` + Sz *xlsxFloatVal `xml:"sz"` + U *xlsxVal `xml:"u"` + VertAlign *xlsxVal `xml:"vertAlign"` + Scheme *xlsxVal `xml:"scheme"` +} + +// xlsxBoolProp handles "CT_BooleanProperty" type which is declared in the XML Schema of Office Open XML. +// XML attribute "val" is optional. If "val" was omitted, the property value becomes "true". +// On the serialization, the struct which has "true" will be serialized an empty XML tag without "val" attributes, +// and the struct which has "false" will not be serialized. +type xlsxBoolProp struct { + Val bool `xml:"val,addr"` +} + +// MarshalXML implements xml.Marshaler interface for xlsxBoolProp +func (b *xlsxBoolProp) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if b.Val { + if err := e.EncodeToken(start); err != nil { + return err + } + if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil { + return err + } + } + return nil +} + +// UnmarshalXML implements xml.Unmarshaler interface for xlsxBoolProp +func (b *xlsxBoolProp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + boolVal := true + for _, attr := range start.Attr { + if attr.Name.Space == "" && attr.Name.Local == "val" { + // supports xsd:boolean + switch attr.Value { + case "true", "1": + boolVal = true + case "false", "0": + boolVal = false + default: + return errors.New( + "Cannot unmarshal into xlsxBoolProp: \"" + + attr.Value + "\" is not a valid boolean value") + } + } + } + b.Val = boolVal + return d.Skip() +} + +// xlsxIntVal is like xlsxVal, except it has an int value +type xlsxIntVal struct { + Val int `xml:"val,attr"` +} + +// xlsxFloatVal is like xlsxVal, except it has a float value +type xlsxFloatVal struct { + Val float64 `xml:"val,attr"` +} + +// xlsxT represents a text. It will be serialized as a XML tag which has character data. +// Attribute xml:space="preserve" will be added to the XML tag if needed. +type xlsxT struct { + Text string `xml:",chardata"` +} + +// MarshalXML implements xml.Marshaler interface for xlsxT +func (t *xlsxT) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if needPreserve(t.Text) { + attr := xml.Attr{ + Name: xml.Name{Local: "xml:space"}, + Value: "preserve", + } + start.Attr = append(start.Attr, attr) + } + + if err := e.EncodeToken(start); err != nil { + return err + } + if err := e.EncodeToken(xml.CharData(t.Text)); err != nil { + return err + } + if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil { + return err + } + return nil +} + +// getText is a nil-safe utility function that gets a string from xlsxT. +// If the pointer of xlsxT was nil, returns an empty string. +func (t *xlsxT) getText() string { + if t == nil { + return "" + } + return t.Text +} + +// needPreserve determines whether xml:space="preserve" is needed. +func needPreserve(s string) bool { + if len(s) == 0 { + return false + } + // Note: + // xml:space="preserve" is not needed for CR and TAB + // because they are serialized as " " and " ". + c := s[0] + if c <= 32 && c != 9 && c != 13 { + return true + } + c = s[len(s)-1] + if c <= 32 && c != 9 && c != 13 { + return true + } + return strings.ContainsRune(s, '\u000a') } diff --git a/xmlSharedStrings_test.go b/xmlSharedStrings_test.go index b9fa8b4c..42bbbfd5 100644 --- a/xmlSharedStrings_test.go +++ b/xmlSharedStrings_test.go @@ -17,8 +17,8 @@ func (s *SharedStringsSuite) SetUpTest(c *C) { s.SharedStringsXML = bytes.NewBufferString( ` + count="5" + uniqueCount="5"> Foo @@ -31,6 +31,39 @@ func (s *SharedStringsSuite) SetUpTest(c *C) { Quuk + + + Normal + + + + + Normal2 + + + + + + + + + + Bools + + + + + + Font Spec + + + + + + + Misc + + `) } @@ -40,9 +73,138 @@ func (s *SharedStringsSuite) TestUnmarshallSharedStrings(c *C) { sst := new(xlsxSST) err := xml.NewDecoder(s.SharedStringsXML).Decode(sst) c.Assert(err, IsNil) - c.Assert(sst.Count, Equals, 4) - c.Assert(sst.UniqueCount, Equals, 4) - c.Assert(sst.SI, HasLen, 4) + c.Assert(sst.Count, Equals, 5) + c.Assert(sst.UniqueCount, Equals, 5) + c.Assert(sst.SI, HasLen, 5) + si := sst.SI[0] - c.Assert(si.T, Equals, "Foo") + c.Assert(si.T.Text, Equals, "Foo") + c.Assert(si.R, IsNil) + si = sst.SI[1] + c.Assert(si.T.Text, Equals, "Bar") + c.Assert(si.R, IsNil) + si = sst.SI[2] + c.Assert(si.T.Text, Equals, "Baz ") + c.Assert(si.R, IsNil) + si = sst.SI[3] + c.Assert(si.T.Text, Equals, "Quuk") + c.Assert(si.R, IsNil) + si = sst.SI[4] + c.Assert(si.T, IsNil) + c.Assert(len(si.R), Equals, 5) + r := si.R[0] + c.Assert(r.T.Text, Equals, "Normal") + c.Assert(r.RPr, IsNil) + r = si.R[1] + c.Assert(r.T.Text, Equals, "Normal2") + c.Assert(r.RPr.RFont, IsNil) + c.Assert(r.RPr.Sz, IsNil) + c.Assert(r.RPr.Color, IsNil) + c.Assert(r.RPr.Family, IsNil) + c.Assert(r.RPr.Charset, IsNil) + c.Assert(r.RPr.Scheme, IsNil) + c.Assert(r.RPr.B.Val, Equals, false) + c.Assert(r.RPr.I.Val, Equals, false) + c.Assert(r.RPr.Strike.Val, Equals, false) + c.Assert(r.RPr.Outline.Val, Equals, false) + c.Assert(r.RPr.Shadow.Val, Equals, false) + c.Assert(r.RPr.Condense.Val, Equals, false) + c.Assert(r.RPr.Extend.Val, Equals, false) + c.Assert(r.RPr.U, IsNil) + c.Assert(r.RPr.VertAlign, IsNil) + r = si.R[2] + c.Assert(r.T.Text, Equals, "Bools") + c.Assert(r.RPr.RFont, IsNil) + c.Assert(r.RPr.B.Val, Equals, true) + c.Assert(r.RPr.I.Val, Equals, false) + c.Assert(r.RPr.Strike.Val, Equals, true) + c.Assert(r.RPr.Condense.Val, Equals, true) + c.Assert(r.RPr.Extend.Val, Equals, false) + r = si.R[3] + c.Assert(r.T.Text, Equals, "Font Spec") + c.Assert(r.RPr.RFont.Val, Equals, "FontZ") + c.Assert(r.RPr.Sz.Val, Equals, 13.5) + c.Assert(*r.RPr.Color.Theme, Equals, 1) + c.Assert(r.RPr.Family.Val, Equals, 2) + c.Assert(r.RPr.Charset.Val, Equals, 128) + c.Assert(r.RPr.Scheme.Val, Equals, "minor") + r = si.R[4] + c.Assert(r.T.Text, Equals, "Misc") + c.Assert(r.RPr.U.Val, Equals, "single") + c.Assert(r.RPr.VertAlign.Val, Equals, "superscript") +} + +// TestMarshalSI_T tests that xlsxT is marshaled as it is expected. +func (s *SharedStringsSuite) TestMarshalSI_T(c *C) { + testMarshalSIT(c, "", "") + testMarshalSIT(c, "a b c", "a b c") + testMarshalSIT(c, " abc", " abc") + testMarshalSIT(c, "abc ", "abc ") + testMarshalSIT(c, "\nabc", "\nabc") + testMarshalSIT(c, "abc\n", "abc\n") + testMarshalSIT(c, "ab\nc", "ab\nc") +} + +func testMarshalSIT(c *C, t string, expected string) { + si := xlsxSI{T: &xlsxT{Text: t}} + bytes, err := xml.Marshal(&si) + c.Assert(err, IsNil) + c.Assert(string(bytes), Equals, expected) +} + +// TestMarshalSI_R tests that xlsxR is marshaled as it is expected. +func (s *SharedStringsSuite) TestMarshalSI_R(c *C) { + testMarshalSIR(c, xlsxR{}, "") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a b c"}}, "a b c") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: " abc"}}, " abc") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "abc "}}, "abc ") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "\nabc"}}, "\nabc") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "abc\n"}}, "abc\n") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "ab\nc"}}, "ab\nc") + + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{RFont: &xlsxVal{Val: "Times New Roman"}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Charset: &xlsxIntVal{Val: 1}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Family: &xlsxIntVal{Val: 1}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{B: xlsxBoolProp{Val: true}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{I: xlsxBoolProp{Val: true}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Strike: xlsxBoolProp{Val: true}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Outline: xlsxBoolProp{Val: true}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Shadow: xlsxBoolProp{Val: true}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Condense: xlsxBoolProp{Val: true}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Extend: xlsxBoolProp{Val: true}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Color: &xlsxColor{RGB: "FF123456"}}}, + "a") + colorIndex := 11 + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Color: &xlsxColor{Indexed: &colorIndex}}}, + "a") + colorTheme := 5 + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Color: &xlsxColor{Theme: &colorTheme}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Color: &xlsxColor{Theme: &colorTheme, Tint: 0.1}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Sz: &xlsxFloatVal{Val: 12.5}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{U: &xlsxVal{Val: "single"}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{VertAlign: &xlsxVal{Val: "superscript"}}}, + "a") + testMarshalSIR(c, xlsxR{T: xlsxT{Text: "a"}, RPr: &xlsxRunProperties{Scheme: &xlsxVal{Val: "major"}}}, + "a") +} + +func testMarshalSIR(c *C, r xlsxR, expected string) { + si := xlsxSI{R: []xlsxR{r}} + bytes, err := xml.Marshal(&si) + c.Assert(err, IsNil) + c.Assert(string(bytes), Equals, expected) }