diff --git a/common/contenttypes.go b/common/contenttypes.go index 406e72759f..a17839ac5e 100644 --- a/common/contenttypes.go +++ b/common/contenttypes.go @@ -108,3 +108,24 @@ func (c ContentTypes) RemoveOverride(path string) { } } } + +// CopyOverride copies override content type for a given `path` and puts it with a path `newPath`. +func (c ContentTypes) CopyOverride(path, newPath string) { + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + if !strings.HasPrefix(newPath, "/") { + newPath = "/" + newPath + } + + for i := range c.x.Override { + if c.x.Override[i].PartNameAttr == path { + copied := *c.x.Override[i] + + copied.PartNameAttr = newPath + + c.x.Override = append(c.x.Override, &copied) + } + } +} diff --git a/common/contenttypes_test.go b/common/contenttypes_test.go index 7f414245fa..92c6302105 100644 --- a/common/contenttypes_test.go +++ b/common/contenttypes_test.go @@ -37,3 +37,22 @@ func TestContentTypesUnmarshal(t *testing.T) { testhelper.CompareGoldenXML(t, "contenttypes.xml", got.Bytes()) } + +func TestCopyOverride(t *testing.T) { + ct := common.NewContentTypes() + ct.AddOverride("/foo/bar.xml", "application/xml") + + lenBefore := len(ct.X().Override) + + ct.CopyOverride("foo/bar.xml", "foo/bar2.xml") + + if len(ct.X().Override) != (lenBefore + 1) { + t.Errorf("expected override len %d, got %d", lenBefore+1, len(ct.X().Override)) + } + + copyIdx := len(ct.X().Override) - 1 + + if ct.X().Override[copyIdx].PartNameAttr != "/foo/bar2.xml" { + t.Errorf("expected \"/foo/bar2.xml\" PartNameAttr, go %s", ct.X().Override[copyIdx].PartNameAttr) + } +} diff --git a/common/relationships.go b/common/relationships.go index 56e870bb5c..c59ccddc58 100644 --- a/common/relationships.go +++ b/common/relationships.go @@ -25,6 +25,12 @@ func NewRelationships() Relationships { return Relationships{x: relationships.NewRelationships()} } +// NewRelationshipsCopy creates a new relationships wrapper as a copy of passed in instance. +func NewRelationshipsCopy(rels Relationships) Relationships { + copiedBody := *rels.x + return Relationships{x: &copiedBody} +} + // X returns the underlying raw XML data. func (r Relationships) X() *relationships.Relationships { return r.x @@ -97,6 +103,35 @@ func (r Relationships) Remove(rel Relationship) bool { return false } +// CopyRelationship copies the relationship. +func (r Relationships) CopyRelationship(idAttr string) (Relationship, bool) { + for i := range r.x.Relationship { + if r.x.Relationship[i].IdAttr == idAttr { + copied := *r.x.Relationship[i] + + nextID := len(r.x.Relationship) + 1 + used := map[string]struct{}{} + + // identify IDs in use + for _, exRel := range r.x.Relationship { + used[exRel.IdAttr] = struct{}{} + } + // find the next ID that is unused + for _, ok := used[fmt.Sprintf("rId%d", nextID)]; ok; _, ok = used[fmt.Sprintf("rId%d", nextID)] { + nextID++ + } + + copied.IdAttr = fmt.Sprintf("rId%d", nextID) + + r.x.Relationship = append(r.x.Relationship, &copied) + + return Relationship{&copied}, true + } + } + + return Relationship{}, false +} + // Hyperlink is just an appropriately configured relationship. type Hyperlink Relationship diff --git a/common/relationships_test.go b/common/relationships_test.go index e5c7fe42ad..8be0fb036e 100644 --- a/common/relationships_test.go +++ b/common/relationships_test.go @@ -124,3 +124,32 @@ func TestRelationshipsRemoval(t *testing.T) { t.Errorf("expected 0, got %d", len(r.Relationships())) } } + +func TestCopyRelationship(t *testing.T) { + r := common.NewRelationships() + r.AddRelationship("foo1", "http://bar") + r.AddRelationship("foo2", "http://bar") + r.AddRelationship("foo3", "http://bar") + + if len(r.Relationships()) != 3 { + t.Errorf("expected 3, got %d", len(r.Relationships())) + } + + copied, ok := r.CopyRelationship(r.Relationships()[1].ID()) + if !ok { + t.Errorf("expected true, got %v", ok) + } + + if len(r.Relationships()) != 4 { + t.Errorf("expected 4, got %d", len(r.Relationships())) + } + + if got := copied.Target(); got != "foo2" { + t.Errorf("expected foo2, got %s", got) + } + + _, ok = r.CopyRelationship("qweqwe") + if ok { + t.Errorf("expected false, got %v", ok) + } +} diff --git a/spreadsheet/testdata/sheets.xlsx b/spreadsheet/testdata/sheets.xlsx new file mode 100644 index 0000000000..bc677686ac Binary files /dev/null and b/spreadsheet/testdata/sheets.xlsx differ diff --git a/spreadsheet/workbook.go b/spreadsheet/workbook.go index 196fcb7830..67826b0a24 100644 --- a/spreadsheet/workbook.go +++ b/spreadsheet/workbook.go @@ -98,6 +98,134 @@ func (wb *Workbook) AddSheet() Sheet { return Sheet{wb, rs, ws} } +// RemoveSheet removes the sheet with the given index from the workbook. +func (wb *Workbook) RemoveSheet(ind int) error { + if wb.SheetCount() <= ind { + return ErrorNotFound + } + + for _, r := range wb.wbRels.Relationships() { + if r.ID() == wb.x.Sheets.Sheet[ind].IdAttr { + wb.wbRels.Remove(r) + break + } + } + + wb.ContentTypes.RemoveOverride(unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet, + unioffice.WorksheetContentType, ind+1)) + + copy(wb.xws[ind:], wb.xws[ind+1:]) + wb.xws = wb.xws[:len(wb.xws)-1] + + removed := wb.x.Sheets.Sheet[ind] + + copy(wb.x.Sheets.Sheet[ind:], wb.x.Sheets.Sheet[ind+1:]) + wb.x.Sheets.Sheet = wb.x.Sheets.Sheet[:len(wb.x.Sheets.Sheet)-1] + + // fix sheet IDs by decrementing each one after the removed sheet + for i := range wb.x.Sheets.Sheet { + if wb.x.Sheets.Sheet[i].SheetIdAttr > removed.SheetIdAttr { + wb.x.Sheets.Sheet[i].SheetIdAttr-- + } + } + + copy(wb.xwsRels[ind:], wb.xwsRels[ind+1:]) + wb.xwsRels = wb.xwsRels[:len(wb.xwsRels)-1] + + copy(wb.comments[ind:], wb.comments[ind+1:]) + wb.comments = wb.comments[:len(wb.comments)-1] + + return nil +} + +// RemoveSheetByName removes the sheet with the given name from the workbook. +func (wb *Workbook) RemoveSheetByName(name string) error { + sheetInd := -1 + for i, s := range wb.Sheets() { + if name == s.Name() { + sheetInd = i + break + } + } + + if sheetInd == -1 { + return ErrorNotFound + } + + return wb.RemoveSheet(sheetInd) +} + +// CopySheet copies the existing sheet at index `ind` and puts its copy with the name `copiedSheetName`. +func (wb *Workbook) CopySheet(ind int, copiedSheetName string) (Sheet, error) { + if wb.SheetCount() <= ind { + return Sheet{}, ErrorNotFound + } + + var copiedRel common.Relationship + for _, r := range wb.wbRels.Relationships() { + if r.ID() == wb.x.Sheets.Sheet[ind].IdAttr { + var ok bool + if copiedRel, ok = wb.wbRels.CopyRelationship(r.ID()); !ok { + return Sheet{}, ErrorNotFound + } + + break + } + } + + wb.ContentTypes.CopyOverride(unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet, + unioffice.WorksheetContentType, ind+1), unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet, + unioffice.WorksheetContentType, len(wb.ContentTypes.X().Override))) + + copiedWs := *wb.xws[ind] + wb.xws = append(wb.xws, &copiedWs) + + var nextSheetID uint32 = 0 + for _, s := range wb.x.Sheets.Sheet { + if s.SheetIdAttr > nextSheetID { + nextSheetID = s.SheetIdAttr + } + } + nextSheetID++ + + copiedSheet := *wb.x.Sheets.Sheet[ind] + copiedSheet.IdAttr = copiedRel.ID() + copiedSheet.NameAttr = copiedSheetName + copiedSheet.SheetIdAttr = nextSheetID + + wb.x.Sheets.Sheet = append(wb.x.Sheets.Sheet, &copiedSheet) + + copiedXwsRel := common.NewRelationshipsCopy(wb.xwsRels[ind]) + wb.xwsRels = append(wb.xwsRels, copiedXwsRel) + + copiedCommentsPtr := wb.comments[ind] + if copiedCommentsPtr == nil { + wb.comments = append(wb.comments, nil) + } else { + copiedComments := *copiedCommentsPtr + wb.comments = append(wb.comments, &copiedComments) + } + + return Sheet{wb, &copiedSheet, &copiedWs}, nil +} + +// CopySheetByName copies the existing sheet with the name `name` and puts its copy with the name `copiedSheetName`. +func (wb *Workbook) CopySheetByName(name, copiedSheetName string) (Sheet, error) { + sheetInd := -1 + for i, s := range wb.Sheets() { + if name == s.Name() { + sheetInd = i + break + } + } + + if sheetInd == -1 { + return Sheet{}, ErrorNotFound + } + + return wb.CopySheet(sheetInd, copiedSheetName) +} + // SaveToFile writes the workbook out to a file. func (wb *Workbook) SaveToFile(path string) error { f, err := os.Create(path) diff --git a/spreadsheet/workbook_test.go b/spreadsheet/workbook_test.go index cc6def8b72..d91562e08b 100644 --- a/spreadsheet/workbook_test.go +++ b/spreadsheet/workbook_test.go @@ -290,3 +290,117 @@ func TestOpenOrderedSheets(t *testing.T) { } } + +func TestRemoveSheet(t *testing.T) { + wb, err := spreadsheet.Open("./testdata/sheets.xlsx") + defer wb.Close() + if err != nil { + t.Fatalf("error opening workbook: %s", err) + } + + wasCount := wb.SheetCount() + + if err := wb.RemoveSheet(15); err == nil { + t.Fatalf("invalid sheet index, expected error %v, got nil", spreadsheet.ErrorNotFound) + } + + if err := wb.RemoveSheet(2); err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if err := wb.Validate(); err != nil { + t.Fatalf("produced invalid workbook: %v", err) + } + + if wb.SheetCount() != (wasCount - 1) { + t.Fatalf("expected sheets count %d, got %d", wasCount-1, wb.SheetCount()) + } +} + +func TestRemoveSheetByName(t *testing.T) { + wb, err := spreadsheet.Open("./testdata/sheets.xlsx") + defer wb.Close() + if err != nil { + t.Fatalf("error opening workbook: %s", err) + } + + wasCount := wb.SheetCount() + + if err := wb.RemoveSheetByName("Sheet156"); err == nil { + t.Fatalf("invalid sheet name, expected error %v, got nil", spreadsheet.ErrorNotFound) + } + + if err := wb.RemoveSheetByName("Sheet2"); err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if err := wb.Validate(); err != nil { + t.Fatalf("produced invalid workbook: %v", err) + } + + if wb.SheetCount() != (wasCount - 1) { + t.Fatalf("expected sheets count %d, got %d", wasCount-1, wb.SheetCount()) + } +} + +func TestCopySheet(t *testing.T) { + wb, err := spreadsheet.Open("./testdata/sheets.xlsx") + defer wb.Close() + if err != nil { + t.Fatalf("error opening workbook: %s", err) + } + + wasCount := wb.SheetCount() + + if _, err := wb.CopySheet(15, "Copied Sheet"); err == nil { + t.Fatalf("invalid sheet index, expected error %v, got nil", spreadsheet.ErrorNotFound) + } + + copiedSheet, err := wb.CopySheet(1, "Copied Sheet") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if err := wb.Validate(); err != nil { + t.Fatalf("produced invalid workbook: %v", err) + } + + if copiedSheet.Name() != "Copied Sheet" { + t.Fatalf("invalid name in the copied sheet, expected \"Copied Sheet\", got \"%s\"", copiedSheet.Name()) + } + + if wb.SheetCount() != (wasCount + 1) { + t.Fatalf("expected sheets count %d, got %d", wasCount+1, wb.SheetCount()) + } +} + +func TestCopySheetByName(t *testing.T) { + wb, err := spreadsheet.Open("./testdata/sheets.xlsx") + defer wb.Close() + if err != nil { + t.Fatalf("error opening workbook: %s", err) + } + + wasCount := wb.SheetCount() + + if _, err := wb.CopySheetByName("Sheet156", "Copied Sheet"); err == nil { + t.Fatalf("invalid sheet name, expected error %v, got nil", spreadsheet.ErrorNotFound) + } + + copiedSheet, err := wb.CopySheetByName("Sheet2", "Copied Sheet") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if err := wb.Validate(); err != nil { + t.Fatalf("produced invalid workbook: %v", err) + } + + if copiedSheet.Name() != "Copied Sheet" { + t.Fatalf("invalid name in the copied sheet, expected \"Copied Sheet\", got \"%s\"", copiedSheet.Name()) + } + + if wb.SheetCount() != (wasCount + 1) { + t.Fatalf("expected sheets count %d, got %d", wasCount+1, wb.SheetCount()) + } +}